├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── OpenCode.md ├── README.md ├── TODO.md ├── python ├── CONTRIBUTING.md ├── LICENSE ├── PRODUCTION.md ├── PUBLISHING.md ├── README.md ├── REVIEW.md ├── claude_code_sdk │ ├── __init__.py │ ├── client │ │ ├── __init__.py │ │ ├── base.py │ │ ├── chat.py │ │ ├── messages.py │ │ ├── sessions.py │ │ └── tools.py │ ├── exceptions.py │ ├── implementations │ │ ├── __init__.py │ │ ├── cli.py │ │ └── converters.py │ ├── logging.py │ ├── retry.py │ ├── types │ │ └── __init__.py │ ├── utils.py │ └── validation.py ├── examples │ ├── basic.py │ └── streaming.py ├── pyproject.toml ├── requirements-dev.txt ├── scripts │ ├── publish.sh │ ├── publish_modified.sh │ ├── publish_modified_cd.sh │ ├── publish_no_prompts.sh │ └── publish_skip_tests.sh ├── setup.py └── tests │ ├── conftest.py │ ├── test_cli.py │ ├── test_client.py │ ├── test_converters.py │ └── test_integration.py └── typescript ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── TEST-REPORT.md ├── package-lock.json ├── package.json ├── scripts ├── prepare-package.js ├── publish.sh └── test-real-cli.js ├── src ├── client │ ├── base.ts │ ├── chat.ts │ ├── index.ts │ ├── messages.ts │ ├── sessions.ts │ └── tools.ts ├── examples │ ├── basic.ts │ └── streaming.ts ├── implementations │ ├── cli.ts │ └── converters.ts ├── index.ts ├── tests │ ├── cli.test.ts │ ├── client.test.ts │ ├── converters.test.ts │ └── sessions.test.ts └── types │ └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | venv/ 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | *.so 8 | .Python 9 | env/ 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Logs 26 | logs 27 | *.log 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # Runtime data 33 | pids 34 | *.pid 35 | *.seed 36 | *.pid.lock 37 | 38 | # Coverage 39 | coverage/ 40 | .coverage 41 | htmlcov/ 42 | .nyc_output 43 | 44 | # IDEs and editors 45 | .idea/ 46 | .vscode/ 47 | *.swp 48 | *.swo 49 | .project 50 | .classpath 51 | .c9/ 52 | *.launch 53 | .settings/ 54 | *.sublime-workspace 55 | 56 | # OS 57 | .DS_Store 58 | .DS_Store? 59 | ._* 60 | .Spotlight-V100 61 | .Trashes 62 | ehthumbs.db 63 | Thumbs.db 64 | 65 | # Environment 66 | .env 67 | .env.local 68 | .env.development.local 69 | .env.test.local 70 | .env.production.local 71 | 72 | # Claude 73 | .claude/ 74 | .opencode/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Claude Code SDK 2 | 3 | Thank you for your interest in contributing to the Claude Code SDK! This document provides guidelines and instructions for contributing to this project. 4 | 5 | ## Code of Conduct 6 | 7 | Please be respectful and considerate of others when contributing to this project. We aim to foster an inclusive and welcoming community. 8 | 9 | ## Repository Structure 10 | 11 | This is a monorepo containing: 12 | 13 | - `typescript/`: TypeScript implementation 14 | - `python/`: Python implementation 15 | 16 | Each implementation has its own set of development tools and workflows. 17 | 18 | ## Getting Started 19 | 20 | 1. **Fork the repository** and clone it locally 21 | 22 | 2. **Choose implementation**: 23 | - For TypeScript contributions: See [TypeScript Contributing Guide](typescript/CONTRIBUTING.md) 24 | - For Python contributions: See [Python Contributing Guide](python/CONTRIBUTING.md) 25 | 26 | ## Pull Request Process 27 | 28 | 1. Follow language-specific guidelines for the implementation you're working on 29 | 2. Update documentation as necessary 30 | 3. Ensure all tests pass 31 | 4. Include a detailed description of your changes in your PR 32 | 5. Reference any relevant issues 33 | 34 | ## Release Process 35 | 36 | Releases are managed by the maintainers. Each implementation has its own release process detailed in its respective CONTRIBUTING.md file. 37 | 38 | ## Getting Help 39 | 40 | If you have questions or need help, please open an issue on GitHub. 41 | 42 | Thank you for contributing to the Claude Code SDK! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /OpenCode.md: -------------------------------------------------------------------------------- 1 | # Claude Code SDK Development Guide 2 | 3 | ## Commands 4 | - Build TypeScript: `npm run build` 5 | - Watch mode: `npm run build:dev` 6 | - Test all: `npm run test` 7 | - Test single file: `npx vitest run src/tests/file.test.ts` 8 | - Lint: `npm run lint` 9 | - Format check: `npm run format` 10 | - Format fix: `npm run format:fix` 11 | - Type check: `npm run typecheck` 12 | - Python install: `pip install -e ./python` 13 | - Python test: `pytest python/tests` 14 | 15 | ## Code Style 16 | - **TypeScript**: 17 | - No semicolons, single quotes, 100 char line limit, 2-space indentation 18 | - Explicit function return types, no `any` type 19 | - camelCase for variables/functions, PascalCase for classes/interfaces 20 | - Use `createError` method with proper status codes 21 | - Named exports preferred over default exports 22 | - ESM modules (type: "module" in package.json) 23 | 24 | - **Python**: 25 | - Follow PEP 8 style guide 26 | - Use type hints for all functions and methods 27 | - Use docstrings for all public functions and classes 28 | - snake_case for variables/functions, PascalCase for classes 29 | - Proper error handling with status codes 30 | - Async/await for streaming operations -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Claude Code SDK 2 | 3 | A wrapper for Claude Code CLI that provides a seamless, type-safe API compatible with both OpenAI and Anthropic SDKs. This monorepo contains both TypeScript and Python implementations. 4 | 5 | ## Overview 6 | 7 | This SDK lets developers interact with Claude Code using familiar OpenAI or Anthropic-style APIs. The SDK wraps the Claude Code CLI, providing both: 8 | 9 | - **TypeScript Implementation**: For Node.js and TypeScript projects 10 | - **Python Implementation**: For Python applications and scripts 11 | 12 | ## Features 13 | 14 | - OpenAI-compatible API methods 15 | - Anthropic-compatible API methods 16 | - Session management for multi-turn conversations 17 | - Tool registration and usage 18 | - Full type support 19 | - Streaming responses 20 | - Batch operations 21 | 22 | ## Implementations 23 | 24 | - [TypeScript SDK](typescript/README.md) 25 | - [Python SDK](python/README.md) 26 | 27 | ## Installation 28 | 29 | Each implementation has its own installation instructions: 30 | 31 | - For TypeScript: See [TypeScript Installation](typescript/README.md#installation) 32 | - For Python: See [Python Installation](python/README.md#installation) 33 | 34 | Both implementations require the Claude Code CLI to be installed: 35 | 36 | ```bash 37 | npm install -g @anthropic-ai/claude-code 38 | ``` 39 | 40 | ## Requirements 41 | 42 | - Node.js v16+ (for TypeScript) 43 | - Python 3.7+ (for Python) 44 | - @anthropic-ai/claude-code CLI installed 45 | 46 | ## Contributing 47 | 48 | Please see the [Contributing Guide](CONTRIBUTING.md) for details on how to contribute to the project. 49 | 50 | ## License 51 | 52 | [MIT](LICENSE) -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Claude Code SDK Review 2 | 3 | This document provides a review of the Claude Code SDK implementations in both TypeScript and Python. The SDK serves as a wrapper around the Claude Code CLI, offering OpenAI and Anthropic compatible APIs for developers. 4 | 5 | ## TypeScript Implementation 6 | 7 | The TypeScript implementation provides a robust wrapper around the Claude Code CLI with features including: 8 | 9 | - **OpenAI-compatible API**: Use `chat.completions.create()` with familiar parameters 10 | - **Anthropic-compatible API**: Use `messages.create()` with Anthropic's message format 11 | - **Session Management**: Create and resume conversations with `sessions.create()` 12 | - **Tool Support**: Register and use tools with `tools.create()` 13 | - **Streaming Support**: Stream responses with `createStream()` methods 14 | - **Error Handling**: Consistent error handling with status codes 15 | 16 | ## Python Implementation 17 | 18 | The Python implementation follows a similar pattern to the TypeScript version, with a clean architecture that provides OpenAI and Anthropic compatibility. 19 | 20 | ### Strengths 21 | - Well-structured modular design with clear separation of concerns 22 | - Comprehensive type hints throughout the codebase 23 | - Robust error handling with specific exception types 24 | - Good test coverage with mock objects 25 | - Clear documentation and examples 26 | - Proper subprocess management for CLI communication 27 | - Retry mechanism for transient errors 28 | 29 | ## Recommendations 30 | 31 | Based on the review of both implementations, here are recommendations for improvement: 32 | 33 | ### General Recommendations 34 | 1. **Documentation Improvements**: 35 | - Add more advanced use case examples 36 | - Create a troubleshooting guide for common issues 37 | - Document versioning and compatibility strategy with the CLI 38 | 39 | 2. **API Design**: 40 | - Consider adding middleware support for logging, metrics, etc. 41 | - Develop a more consistent approach to option merging across both implementations 42 | 43 | 3. **Testing**: 44 | - Add integration tests with the actual CLI 45 | - Add more edge case tests (network failures, CLI crashes) 46 | - Implement performance benchmarks 47 | 48 | ### Python-Specific Recommendations 49 | 1. **Performance Improvements**: 50 | - Implement true parallelism in the `batch_create` method rather than sequential execution 51 | - Consider a connection pool for CLI processes to reduce startup overhead 52 | 53 | 2. **Tool Persistence**: 54 | - Add the ability to persist tool registrations between sessions 55 | - Implement disk-based caching for tools with complex schemas 56 | 57 | 3. **Logging Enhancements**: 58 | - Add more granular log levels 59 | - Implement structured logging for better analysis 60 | - Add optional request/response logging for debugging 61 | 62 | 4. **CLI Communication**: 63 | - Optimize CLI communication by reducing redundant data transfer 64 | - Consider implementing a long-running CLI process mode for better performance 65 | 66 | 5. **Async Support**: 67 | - Expand async support across more of the API 68 | - Add optional asyncio event loop parameter 69 | 70 | ### TypeScript-Specific Recommendations 71 | 1. **Error Handling**: 72 | - Improve consistency in error types between OpenAI and Anthropic APIs 73 | - Add more granular error categories 74 | 75 | 2. **Type Safety**: 76 | - Strengthen generic typing for request/response objects 77 | - Add runtime type validation for critical parameters 78 | 79 | 3. **Configuration**: 80 | - Add support for configuration from environment variables 81 | - Implement a configuration provider interface 82 | 83 | ## Future Directions 84 | 1. **WebSocket Support**: 85 | - Investigate WebSocket communication with Claude Code for reduced latency 86 | 87 | 2. **Caching Layer**: 88 | - Implement optional response caching for common prompts 89 | - Add LRU cache for frequently used operations 90 | 91 | 3. **Direct API Integration**: 92 | - Consider direct API integration options as an alternative to CLI for production use 93 | 94 | 4. **Monitoring**: 95 | - Add telemetry hooks for monitoring and observability 96 | - Implement token counting and usage tracking 97 | 98 | 5. **Versioning Strategy**: 99 | - Develop a clear versioning strategy aligned with Claude Code CLI releases 100 | - Document upgrade paths and breaking changes 101 | 102 | These recommendations aim to enhance both the Python and TypeScript implementations of the Claude Code SDK, improving developer experience, performance, and maintainability. -------------------------------------------------------------------------------- /python/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Claude Code SDK (Python) 2 | 3 | Thank you for your interest in contributing to the Claude Code SDK! This document provides guidelines and instructions for contributing to the Python implementation. 4 | 5 | ## Code of Conduct 6 | 7 | Please be respectful and considerate of others when contributing to this project. We aim to foster an inclusive and welcoming community. 8 | 9 | ## Getting Started 10 | 11 | 1. **Fork the repository** and clone it locally 12 | 2. **Set up a virtual environment**: 13 | ```bash 14 | python -m venv venv 15 | source venv/bin/activate # On Windows: venv\Scripts\activate 16 | ``` 17 | 3. **Install in development mode**: 18 | ```bash 19 | pip install -e . 20 | pip install -r requirements-dev.txt # Install development dependencies 21 | ``` 22 | 4. **Run tests**: 23 | ```bash 24 | pytest 25 | ``` 26 | 27 | ## Development Workflow 28 | 29 | 1. **Create a branch** for your feature or bugfix: 30 | ```bash 31 | git checkout -b feature/your-feature-name 32 | ``` 33 | 34 | 2. **Make your changes** following the code style guidelines 35 | 36 | 3. **Write tests** for your changes 37 | 38 | 4. **Run linting and type checking**: 39 | ```bash 40 | mypy claude_code_sdk 41 | flake8 claude_code_sdk 42 | black --check claude_code_sdk 43 | ``` 44 | 45 | 5. **Format your code**: 46 | ```bash 47 | black claude_code_sdk 48 | isort claude_code_sdk 49 | ``` 50 | 51 | 6. **Commit your changes** with a descriptive commit message 52 | 53 | 7. **Push your branch** and submit a pull request 54 | 55 | ## Code Style Guidelines 56 | 57 | - Follow PEP 8 style guidelines 58 | - Use type hints for all functions and methods 59 | - Write docstrings for all public functions, methods, and classes 60 | - Use snake_case for variables/functions and PascalCase for classes 61 | - Handle errors appropriately with specific exception types 62 | - Use async/await for streaming operations where appropriate 63 | 64 | ## Testing 65 | 66 | - Write unit tests for all new functionality 67 | - Ensure all tests pass before submitting a pull request 68 | - Mock external dependencies in tests 69 | - Use pytest fixtures for common test setup 70 | 71 | ## Documentation 72 | 73 | - Update documentation for any changed functionality 74 | - Document all public APIs with docstrings 75 | - Include examples for new features 76 | - Follow Google-style docstring format 77 | 78 | ## Pull Request Process 79 | 80 | 1. Ensure your code follows the style guidelines 81 | 2. Update the README.md with details of changes if appropriate 82 | 3. The PR should work on the main branch 83 | 4. Include a description of the changes in your PR 84 | 85 | ## Release Process 86 | 87 | Releases are managed by the maintainers. The general process is: 88 | 89 | 1. Update version in `__init__.py` and setup.py 90 | 2. Update CHANGELOG.md 91 | 3. Create a new release on GitHub 92 | 4. Publish to PyPI 93 | 94 | ## Getting Help 95 | 96 | If you have questions or need help, please open an issue on GitHub. 97 | 98 | Thank you for contributing to the Claude Code SDK! -------------------------------------------------------------------------------- /python/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /python/PRODUCTION.md: -------------------------------------------------------------------------------- 1 | # Claude Code SDK Python Implementation - Production Ready 2 | 3 | I've enhanced the Python implementation of the Claude Code SDK to make it production-ready with the following improvements: 4 | 5 | ## Key Enhancements 6 | 7 | 1. **Robust Error Handling** 8 | - Comprehensive exception hierarchy with specific error types 9 | - Detailed error messages with status codes and request IDs 10 | - Proper error parsing from CLI output 11 | 12 | 2. **Retry Logic** 13 | - Exponential backoff with jitter for transient errors 14 | - Configurable retry policies for different error types 15 | - Proper handling of rate limits 16 | 17 | 3. **Input Validation** 18 | - Parameter validation for required fields, types, and ranges 19 | - Enum validation for restricted values 20 | - Path sanitization for security 21 | 22 | 4. **Logging** 23 | - Configurable logging system 24 | - Environment variable control of log levels 25 | - Detailed logs for debugging 26 | 27 | 5. **Security Improvements** 28 | - Path sanitization to prevent directory traversal 29 | - Input validation to prevent command injection 30 | - Secure handling of API keys 31 | 32 | 6. **Utility Functions** 33 | - System information gathering 34 | - CLI version checking 35 | - Temporary file handling 36 | 37 | ## Implementation Details 38 | 39 | The enhanced implementation follows best practices for production Python libraries: 40 | 41 | - **Type Hints**: Comprehensive type annotations for better IDE support and static analysis 42 | - **Documentation**: Detailed docstrings for all public methods and classes 43 | - **Testing**: Unit tests with mocking for all components 44 | - **Error Handling**: Proper exception hierarchy and error propagation 45 | - **Performance**: Efficient subprocess management and streaming 46 | - **Compatibility**: Support for different Python versions (3.7+) 47 | 48 | ## Usage 49 | 50 | The API remains the same as before, but with improved reliability and error handling: 51 | 52 | ```python 53 | from claude_code_sdk import ClaudeCode, configure_logging 54 | 55 | # Configure logging 56 | configure_logging(level="INFO") 57 | 58 | # Create client 59 | claude = ClaudeCode() 60 | 61 | # Use with better error handling 62 | try: 63 | response = claude.chat["completions"].create({ 64 | "model": "claude-code", 65 | "messages": [ 66 | {"role": "user", "content": "Write a Python function"} 67 | ] 68 | }) 69 | print(response["choices"][0]["message"]["content"]) 70 | except Exception as e: 71 | print(f"Error: {e}") 72 | ``` 73 | 74 | The SDK is now ready for production use with proper error handling, logging, validation, and security measures. -------------------------------------------------------------------------------- /python/PUBLISHING.md: -------------------------------------------------------------------------------- 1 | # Publishing to PyPI 2 | 3 | This guide explains how to publish the Claude Code SDK Python package to PyPI. 4 | 5 | ## Prerequisites 6 | 7 | 1. Make sure you have a PyPI account 8 | 2. Install required publishing tools: 9 | ```bash 10 | python -m pip install --upgrade pip 11 | python -m pip install --upgrade build twine 12 | ``` 13 | 3. If you want to test before publishing to the main PyPI, register on TestPyPI: https://test.pypi.org/account/register/ 14 | 15 | ## Update Version and Documentation 16 | 17 | 1. Update the version number in `setup.py` 18 | 2. Update any necessary documentation 19 | 3. Make sure all tests pass: 20 | ```bash 21 | pytest 22 | ``` 23 | 24 | ## Build the Package 25 | 26 | ```bash 27 | # Clean previous builds 28 | rm -rf dist/ build/ *.egg-info/ 29 | 30 | # Build the package 31 | python -m build 32 | ``` 33 | 34 | This creates source and wheel distributions in the `dist/` directory. 35 | 36 | ## Test the Package (Optional) 37 | 38 | Upload to TestPyPI first to verify everything works: 39 | 40 | ```bash 41 | python -m twine upload --repository testpypi dist/* 42 | ``` 43 | 44 | Install from TestPyPI to verify: 45 | 46 | ```bash 47 | pip install --index-url https://test.pypi.org/simple/ claude-code-sdk 48 | ``` 49 | 50 | ## Publish to PyPI 51 | 52 | Once you're confident everything is working correctly: 53 | 54 | ```bash 55 | # Upload the package to PyPI 56 | python -m twine upload dist/* 57 | ``` 58 | 59 | You'll be prompted for your PyPI username and password. 60 | 61 | ### Security Best Practices 62 | 63 | Avoid typing your password in the terminal by using one of these methods: 64 | 65 | 1. Configure a PyPI token in your `.pypirc` file: 66 | ``` 67 | [pypi] 68 | username = __token__ 69 | password = pypi-xxx... 70 | ``` 71 | 72 | 2. Use environment variables: 73 | ```bash 74 | export TWINE_USERNAME=__token__ 75 | export TWINE_PASSWORD=pypi-xxx... 76 | python -m twine upload dist/* 77 | ``` 78 | 79 | 3. Use keyring to securely store credentials: 80 | ```bash 81 | pip install keyring 82 | keyring set https://upload.pypi.org/legacy/ your-username your-password 83 | python -m twine upload dist/* 84 | ``` 85 | 86 | ## Verify the Upload 87 | 88 | Check that your package appears on PyPI: 89 | https://pypi.org/project/claude-code-sdk/ 90 | 91 | ## Install from PyPI 92 | 93 | Users can now install your package using: 94 | 95 | ```bash 96 | pip install claude-code-sdk 97 | ``` 98 | 99 | ## Creating a GitHub Release 100 | 101 | After publishing to PyPI: 102 | 103 | 1. Create a new tag matching the version: 104 | ```bash 105 | git tag v0.1.0 106 | git push origin v0.1.0 107 | ``` 108 | 109 | 2. Create a release on GitHub with release notes -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Claude Code SDK - Python 2 | 3 | A Python wrapper for Claude Code CLI that provides a seamless, type-safe API compatible with both OpenAI and Anthropic SDKs. 4 | 5 | ## Installation 6 | 7 | First, install the Claude Code CLI: 8 | 9 | ```bash 10 | npm install -g @anthropic-ai/claude-code 11 | ``` 12 | 13 | Then install the wrapper: 14 | 15 | ```bash 16 | pip install claude-code-sdk 17 | ``` 18 | 19 | For development installation: 20 | 21 | ```bash 22 | git clone https://github.com/anthropics/claude-code-sdk.git 23 | cd claude-code-sdk/python 24 | pip install -e . 25 | ``` 26 | 27 | ## Setup 28 | 29 | You'll need an Anthropic API key to use Claude Code. You can either set it as an environment variable: 30 | 31 | ```bash 32 | export ANTHROPIC_API_KEY=your_api_key_here 33 | ``` 34 | 35 | Or provide it when initializing the client: 36 | 37 | ```python 38 | from claude_code_sdk import ClaudeCode 39 | 40 | claude = ClaudeCode(options={ 41 | "api_key": "your_api_key_here" 42 | }) 43 | ``` 44 | 45 | ## Usage 46 | 47 | This SDK provides both OpenAI-style and Anthropic-style APIs for interacting with Claude Code. 48 | 49 | ### OpenAI Style API 50 | 51 | ```python 52 | from claude_code_sdk import ClaudeCode 53 | 54 | # Create a client 55 | claude = ClaudeCode() 56 | 57 | # Use OpenAI-style completions API 58 | def generate_code(): 59 | response = claude.chat["completions"].create({ 60 | "model": "claude-code", 61 | "messages": [ 62 | {"role": "user", "content": "Write a Python function to read CSV files"} 63 | ], 64 | "max_tokens": 1000, 65 | "temperature": 0.7, 66 | }) 67 | 68 | print(response["choices"][0]["message"]["content"]) 69 | 70 | # Streaming example 71 | async def stream_code(): 72 | stream = claude.chat["completions"].create_stream({ 73 | "model": "claude-code", 74 | "messages": [ 75 | {"role": "user", "content": "Create a Python class for a login form"} 76 | ], 77 | "stream": True 78 | }) 79 | 80 | async for chunk in stream: 81 | if "choices" in chunk and chunk["choices"] and "delta" in chunk["choices"][0]: 82 | delta = chunk["choices"][0]["delta"] 83 | if "content" in delta and delta["content"]: 84 | print(delta["content"], end="", flush=True) 85 | ``` 86 | 87 | ### Anthropic Style API 88 | 89 | ```python 90 | from claude_code_sdk import ClaudeCode 91 | 92 | # Create a client 93 | claude = ClaudeCode() 94 | 95 | # Use Anthropic-style messages API 96 | def generate_code(): 97 | response = claude.messages.create({ 98 | "model": "claude-code", 99 | "messages": [ 100 | { 101 | "role": "user", 102 | "content": [ 103 | {"type": "text", "text": "Write a Python function to read CSV files"} 104 | ] 105 | } 106 | ], 107 | "max_tokens": 1000, 108 | }) 109 | 110 | print(response["content"][0]["text"]) 111 | 112 | # Streaming example 113 | async def stream_code(): 114 | stream = claude.messages.create_stream({ 115 | "model": "claude-code", 116 | "messages": [ 117 | { 118 | "role": "user", 119 | "content": [ 120 | {"type": "text", "text": "Create a Python class for a login form"} 121 | ] 122 | } 123 | ], 124 | "stream": True 125 | }) 126 | 127 | async for chunk in stream: 128 | if chunk.get("type") == "content_block_delta" and "delta" in chunk: 129 | delta = chunk["delta"] 130 | if "text" in delta: 131 | print(delta["text"], end="", flush=True) 132 | ``` 133 | 134 | ### Session Management 135 | 136 | ```python 137 | from claude_code_sdk import ClaudeCode 138 | 139 | claude = ClaudeCode() 140 | 141 | def code_session(): 142 | # Start a session 143 | session = claude.sessions.create({ 144 | "messages": [ 145 | {"role": "user", "content": "Let's create a Python project"} 146 | ] 147 | }) 148 | 149 | # Continue the session 150 | response = session.continue_session({ 151 | "messages": [ 152 | {"role": "user", "content": "Now add a database connection"} 153 | ] 154 | }) 155 | 156 | print(response["choices"][0]["message"]["content"]) 157 | ``` 158 | 159 | ### Tools 160 | 161 | ```python 162 | from claude_code_sdk import ClaudeCode 163 | 164 | claude = ClaudeCode() 165 | 166 | def use_tools(): 167 | # Register a tool 168 | claude.tools.create({ 169 | "name": "filesystem", 170 | "description": "Access the filesystem", 171 | "input_schema": { 172 | "type": "object", 173 | "properties": { 174 | "path": {"type": "string"} 175 | }, 176 | "required": ["path"] 177 | } 178 | }) 179 | 180 | # Use the tool in a chat completion 181 | response = claude.chat["completions"].create({ 182 | "model": "claude-code", 183 | "messages": [ 184 | {"role": "user", "content": "Read my README.md file"} 185 | ], 186 | "tools": [{"name": "filesystem"}] 187 | }) 188 | 189 | print(response["choices"][0]["message"]["content"]) 190 | ``` 191 | 192 | ## Debugging 193 | 194 | To test if the Claude Code CLI is installed and configured correctly, run: 195 | 196 | ```bash 197 | npx claude -h 198 | ``` 199 | 200 | If you experience issues, set more verbose output: 201 | 202 | ```python 203 | claude = ClaudeCode(options={ 204 | "api_key": os.environ.get("ANTHROPIC_API_KEY"), 205 | "cli_path": "/path/to/claude", # If claude isn't in your PATH 206 | "timeout": 60000 # Longer timeout (ms) 207 | }) 208 | ``` 209 | 210 | ## Features 211 | 212 | - OpenAI-compatible `chat["completions"].create()` method 213 | - Anthropic-compatible `messages.create()` method 214 | - Session management for multi-turn conversations 215 | - Tool registration and usage 216 | - Full type hints 217 | - Streaming responses 218 | - Batch operations 219 | 220 | ## Requirements 221 | 222 | - Python 3.7+ 223 | - @anthropic-ai/claude-code CLI installed 224 | 225 | ## Development 226 | 227 | ### Publishing to PyPI 228 | 229 | To publish a new version to PyPI, use the provided script: 230 | 231 | ```bash 232 | cd python 233 | ./scripts/publish.sh 234 | ``` 235 | 236 | For detailed instructions, see [PUBLISHING.md](PUBLISHING.md). 237 | 238 | ## License 239 | 240 | MIT -------------------------------------------------------------------------------- /python/REVIEW.md: -------------------------------------------------------------------------------- 1 | # Claude Code SDK Python Implementation 2 | 3 | ## Overview 4 | This is a Python implementation of the Claude Code SDK, providing a wrapper around the Claude Code CLI. The implementation follows the same patterns as the TypeScript version, offering both OpenAI-compatible and Anthropic-compatible APIs. 5 | 6 | ## Key Features 7 | - **OpenAI-compatible API**: Use `chat["completions"].create()` with familiar parameters 8 | - **Anthropic-compatible API**: Use `messages.create()` with Anthropic's message format 9 | - **Session Management**: Create and resume conversations with `sessions.create()` 10 | - **Tool Support**: Register and use tools with `tools.create()` 11 | - **Streaming Support**: Stream responses with `create_stream()` methods 12 | - **Error Handling**: Consistent error handling with status codes 13 | 14 | ## Implementation Details 15 | The Python SDK is structured similarly to the TypeScript version: 16 | 17 | - **Client Classes**: Base client, chat completions, messages, sessions, and tools 18 | - **CLI Executor**: Handles subprocess management and command execution 19 | - **Converters**: Transform between different API formats 20 | - **Type Definitions**: Provide type hints for better IDE support 21 | 22 | ## Alignment with Official SDK 23 | This implementation aligns with Anthropic's official SDK documentation, supporting: 24 | - Basic SDK usage patterns 25 | - Advanced features like multi-turn conversations 26 | - Custom system prompts 27 | - MCP configuration 28 | - Multiple output formats (text, JSON, stream-JSON) 29 | - Proper message schema handling 30 | 31 | ## Usage Examples 32 | The SDK includes example scripts demonstrating: 33 | - Basic usage with OpenAI and Anthropic APIs 34 | - Streaming responses 35 | - Session management 36 | - Tool registration and usage 37 | 38 | ## Development Status 39 | This is a Python implementation of the Claude Code SDK based on the TypeScript version. Anthropic has mentioned that official Python SDKs are coming soon, which may replace or complement this implementation. -------------------------------------------------------------------------------- /python/claude_code_sdk/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Claude Code SDK - Python wrapper for Claude Code CLI 3 | """ 4 | 5 | from claude_code_sdk.client import ClaudeCode 6 | from claude_code_sdk.exceptions import ( 7 | ClaudeCodeError, 8 | AuthenticationError, 9 | RateLimitError, 10 | APIError, 11 | InvalidRequestError, 12 | TimeoutError, 13 | ToolError 14 | ) 15 | from claude_code_sdk.logging import configure_logging 16 | from claude_code_sdk.utils import get_sdk_version, get_cli_version, is_cli_installed 17 | 18 | __version__ = "0.1.0" 19 | __all__ = [ 20 | "ClaudeCode", 21 | "ClaudeCodeError", 22 | "AuthenticationError", 23 | "RateLimitError", 24 | "APIError", 25 | "InvalidRequestError", 26 | "TimeoutError", 27 | "ToolError", 28 | "configure_logging", 29 | "get_sdk_version", 30 | "get_cli_version", 31 | "is_cli_installed" 32 | ] -------------------------------------------------------------------------------- /python/claude_code_sdk/client/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main client for Claude Code SDK 3 | """ 4 | 5 | from claude_code_sdk.client.base import BaseClient 6 | from claude_code_sdk.client.chat import ChatCompletions 7 | from claude_code_sdk.client.messages import Messages 8 | from claude_code_sdk.client.sessions import Sessions 9 | from claude_code_sdk.client.tools import Tools 10 | from claude_code_sdk.types import ClaudeCodeOptions 11 | 12 | 13 | class ClaudeCode(BaseClient): 14 | """Main client for Claude Code SDK""" 15 | 16 | def __init__(self, options: ClaudeCodeOptions = None): 17 | """ 18 | Initialize the Claude Code client 19 | 20 | Args: 21 | options: Configuration options 22 | """ 23 | super().__init__(options) 24 | 25 | # Initialize API components 26 | self.chat = self._init_chat() 27 | self.messages = Messages(self) 28 | self.sessions = Sessions(self) 29 | self.tools = Tools(self) 30 | 31 | def _init_chat(self): 32 | """Initialize chat completions with OpenAI-style interface""" 33 | completions = ChatCompletions(self) 34 | return { 35 | "completions": completions 36 | } -------------------------------------------------------------------------------- /python/claude_code_sdk/client/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base client implementation for Claude Code SDK 3 | """ 4 | 5 | import os 6 | from typing import Dict, Any, Optional 7 | 8 | from claude_code_sdk.implementations.cli import ClaudeCliExecutor, ClaudeExecParams 9 | from claude_code_sdk.types import ClaudeCodeOptions, ClaudeCodeError 10 | 11 | 12 | class BaseClient: 13 | """Base client for Claude Code SDK""" 14 | 15 | def __init__(self, options: Optional[ClaudeCodeOptions] = None): 16 | """ 17 | Initialize the base client 18 | 19 | Args: 20 | options: Configuration options for the client 21 | """ 22 | if options is None: 23 | options = {} 24 | 25 | self.api_key = options.get("api_key") or os.environ.get("ANTHROPIC_API_KEY") 26 | self.default_model = "claude-code" 27 | self.default_timeout = options.get("timeout", 300000) # 5 minutes default 28 | 29 | env = {} 30 | if self.api_key: 31 | env["ANTHROPIC_API_KEY"] = self.api_key 32 | 33 | self.executor = ClaudeCliExecutor( 34 | cli_path=options.get("cli_path", "@anthropic-ai/claude-code"), 35 | timeout=self.default_timeout, 36 | env=env 37 | ) 38 | 39 | def create_error(self, message: str, status: int = 500, code: Optional[str] = None) -> ClaudeCodeError: 40 | """ 41 | Creates an error object in the style of OpenAI/Anthropic SDKs 42 | 43 | Args: 44 | message: Error message 45 | status: HTTP status code 46 | code: Error code 47 | 48 | Returns: 49 | ClaudeCodeError: Formatted error object 50 | """ 51 | error = ClaudeCodeError(message) 52 | error.status = status 53 | error.code = code 54 | return error 55 | 56 | def execute_command(self, params: ClaudeExecParams) -> str: 57 | """ 58 | Executes a Claude CLI command with error handling 59 | 60 | Args: 61 | params: Parameters for the CLI command 62 | 63 | Returns: 64 | str: Command output 65 | 66 | Raises: 67 | ClaudeCodeError: If command execution fails 68 | """ 69 | try: 70 | return self.executor.execute(params) 71 | except Exception as e: 72 | status = getattr(e, "status", 500) 73 | raise self.create_error(str(e), status=status) 74 | 75 | def execute_stream_command(self, params: ClaudeExecParams): 76 | """ 77 | Creates a streaming response from Claude CLI 78 | 79 | Args: 80 | params: Parameters for the CLI command 81 | 82 | Returns: 83 | Generator: Stream of response chunks 84 | """ 85 | return self.executor.execute_stream(params) -------------------------------------------------------------------------------- /python/claude_code_sdk/client/chat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Chat completions API (OpenAI-style) 3 | """ 4 | 5 | from typing import Dict, List, Any, Optional, AsyncIterable, Union 6 | 7 | from claude_code_sdk.client.base import BaseClient 8 | from claude_code_sdk.implementations.converters import ( 9 | convert_messages_to_prompt, 10 | parse_cli_output, 11 | convert_openai_to_anthropic_tools, 12 | ) 13 | 14 | 15 | class ChatCompletions: 16 | """OpenAI-style chat completions API""" 17 | 18 | def __init__(self, client: BaseClient): 19 | """ 20 | Initialize chat completions 21 | 22 | Args: 23 | client: Base client instance 24 | """ 25 | self.client = client 26 | 27 | def create(self, params: Dict[str, Any]) -> Dict[str, Any]: 28 | """ 29 | Create a completion (OpenAI style) 30 | 31 | Args: 32 | params: OpenAI-style parameters 33 | 34 | Returns: 35 | Dict: OpenAI-style completion response 36 | """ 37 | # Convert the OpenAI-style parameters to CLI parameters 38 | prompt = convert_messages_to_prompt(params["messages"]) 39 | 40 | cli_params = { 41 | "prompt": prompt, 42 | "output_format": "json", 43 | "temperature": params.get("temperature"), 44 | "max_tokens": params.get("max_tokens"), 45 | "top_p": params.get("top_p"), 46 | "stop": params.get("stop"), 47 | "timeout": params.get("timeout"), 48 | } 49 | 50 | # Remove None values 51 | cli_params = {k: v for k, v in cli_params.items() if v is not None} 52 | 53 | # Handle stop sequences 54 | if "stop" in cli_params and isinstance(cli_params["stop"], list): 55 | cli_params["stop"] = ",".join(cli_params["stop"]) 56 | 57 | # Handle tools if provided 58 | if "tools" in params and params["tools"]: 59 | anthropic_tools = convert_openai_to_anthropic_tools(params["tools"]) 60 | tool_names = [tool["name"] for tool in anthropic_tools] 61 | cli_params["allowed_tools"] = tool_names 62 | 63 | if params.get("stream"): 64 | # Create streaming response 65 | return self.create_stream(params) 66 | else: 67 | # Execute and parse response 68 | output = self.client.execute_command(cli_params) 69 | return parse_cli_output(output) 70 | 71 | def create_stream(self, params: Dict[str, Any]) -> AsyncIterable[Dict[str, Any]]: 72 | """ 73 | Create a streaming completion (OpenAI style) 74 | 75 | Args: 76 | params: OpenAI-style parameters 77 | 78 | Returns: 79 | AsyncIterable: Stream of completion chunks 80 | """ 81 | # Convert the OpenAI-style parameters to CLI parameters 82 | prompt = convert_messages_to_prompt(params["messages"]) 83 | 84 | cli_params = { 85 | "prompt": prompt, 86 | "output_format": "stream-json", 87 | "temperature": params.get("temperature"), 88 | "max_tokens": params.get("max_tokens"), 89 | "top_p": params.get("top_p"), 90 | "stop": params.get("stop"), 91 | "timeout": params.get("timeout"), 92 | } 93 | 94 | # Remove None values 95 | cli_params = {k: v for k, v in cli_params.items() if v is not None} 96 | 97 | # Handle stop sequences 98 | if "stop" in cli_params and isinstance(cli_params["stop"], list): 99 | cli_params["stop"] = ",".join(cli_params["stop"]) 100 | 101 | # Handle tools if provided 102 | if "tools" in params and params["tools"]: 103 | anthropic_tools = convert_openai_to_anthropic_tools(params["tools"]) 104 | tool_names = [tool["name"] for tool in anthropic_tools] 105 | cli_params["allowed_tools"] = tool_names 106 | 107 | # Get streaming response 108 | return self.client.execute_stream_command(cli_params) 109 | 110 | async def batch_create(self, params_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 111 | """ 112 | Batch create completions (custom extension) 113 | 114 | Args: 115 | params_list: List of OpenAI-style parameters 116 | 117 | Returns: 118 | List[Dict]: List of completion responses 119 | """ 120 | results = [] 121 | for params in params_list: 122 | results.append(self.create(params)) 123 | return results -------------------------------------------------------------------------------- /python/claude_code_sdk/client/messages.py: -------------------------------------------------------------------------------- 1 | """ 2 | Messages API (Anthropic-style) 3 | """ 4 | 5 | from typing import Dict, List, Any, Optional, AsyncIterable 6 | 7 | from claude_code_sdk.client.base import BaseClient 8 | from claude_code_sdk.implementations.converters import ( 9 | convert_anthropic_messages_to_prompt, 10 | parse_cli_output, 11 | convert_anthropic_to_openai_response, 12 | ) 13 | 14 | 15 | class Messages: 16 | """Anthropic-style messages API""" 17 | 18 | def __init__(self, client: BaseClient): 19 | """ 20 | Initialize messages API 21 | 22 | Args: 23 | client: Base client instance 24 | """ 25 | self.client = client 26 | 27 | def create(self, params: Dict[str, Any]) -> Dict[str, Any]: 28 | """ 29 | Create a message (Anthropic style) 30 | 31 | Args: 32 | params: Anthropic-style parameters 33 | 34 | Returns: 35 | Dict: Anthropic-style message response 36 | """ 37 | # Convert the Anthropic-style parameters to CLI parameters 38 | prompt = convert_anthropic_messages_to_prompt(params["messages"]) 39 | 40 | cli_params = { 41 | "prompt": prompt, 42 | "output_format": "json", 43 | "temperature": params.get("temperature"), 44 | "max_tokens": params.get("max_tokens"), 45 | "top_p": params.get("top_p"), 46 | "stop_sequences": params.get("stop_sequences"), 47 | "timeout": params.get("timeout"), 48 | } 49 | 50 | # Remove None values 51 | cli_params = {k: v for k, v in cli_params.items() if v is not None} 52 | 53 | # Handle stop sequences 54 | if "stop_sequences" in cli_params and isinstance(cli_params["stop_sequences"], list): 55 | cli_params["stop"] = ",".join(cli_params["stop_sequences"]) 56 | del cli_params["stop_sequences"] 57 | 58 | # Handle tools if provided 59 | if "tools" in params and params["tools"]: 60 | tool_names = [tool["name"] for tool in params["tools"]] 61 | cli_params["allowed_tools"] = tool_names 62 | 63 | if params.get("stream"): 64 | # Create streaming response 65 | return self.create_stream(params) 66 | else: 67 | # Execute and parse response 68 | output = self.client.execute_command(cli_params) 69 | response = parse_cli_output(output) 70 | 71 | # Convert to Anthropic format if needed 72 | return convert_anthropic_to_openai_response(response) 73 | 74 | def create_stream(self, params: Dict[str, Any]) -> AsyncIterable[Dict[str, Any]]: 75 | """ 76 | Create a streaming message (Anthropic style) 77 | 78 | Args: 79 | params: Anthropic-style parameters 80 | 81 | Returns: 82 | AsyncIterable: Stream of message chunks 83 | """ 84 | # Convert the Anthropic-style parameters to CLI parameters 85 | prompt = convert_anthropic_messages_to_prompt(params["messages"]) 86 | 87 | cli_params = { 88 | "prompt": prompt, 89 | "output_format": "stream-json", 90 | "temperature": params.get("temperature"), 91 | "max_tokens": params.get("max_tokens"), 92 | "top_p": params.get("top_p"), 93 | "stop_sequences": params.get("stop_sequences"), 94 | "timeout": params.get("timeout"), 95 | } 96 | 97 | # Remove None values 98 | cli_params = {k: v for k, v in cli_params.items() if v is not None} 99 | 100 | # Handle stop sequences 101 | if "stop_sequences" in cli_params and isinstance(cli_params["stop_sequences"], list): 102 | cli_params["stop"] = ",".join(cli_params["stop_sequences"]) 103 | del cli_params["stop_sequences"] 104 | 105 | # Handle tools if provided 106 | if "tools" in params and params["tools"]: 107 | tool_names = [tool["name"] for tool in params["tools"]] 108 | cli_params["allowed_tools"] = tool_names 109 | 110 | # Get streaming response 111 | return self.client.execute_stream_command(cli_params) -------------------------------------------------------------------------------- /python/claude_code_sdk/client/sessions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Session management for Claude Code SDK 3 | """ 4 | 5 | from typing import Dict, List, Any, Optional 6 | 7 | from claude_code_sdk.client.base import BaseClient 8 | from claude_code_sdk.client.chat import ChatCompletions 9 | 10 | 11 | class Session: 12 | """Claude Code session""" 13 | 14 | def __init__(self, client: BaseClient, session_id: str): 15 | """ 16 | Initialize a session 17 | 18 | Args: 19 | client: Base client instance 20 | session_id: Session ID 21 | """ 22 | self.client = client 23 | self.id = session_id 24 | self.chat = ChatCompletions(client) 25 | 26 | def continue_session(self, params: Dict[str, Any]) -> Dict[str, Any]: 27 | """ 28 | Continue the session with new messages 29 | 30 | Args: 31 | params: Parameters for continuation 32 | 33 | Returns: 34 | Dict: Response from Claude 35 | """ 36 | # Add session ID to parameters 37 | continue_params = params.copy() 38 | continue_params["resume"] = self.id 39 | 40 | # Use chat completions to handle the request 41 | return self.chat.create(continue_params) 42 | 43 | def continue_stream(self, params: Dict[str, Any]): 44 | """ 45 | Continue the session with streaming 46 | 47 | Args: 48 | params: Parameters for continuation 49 | 50 | Returns: 51 | AsyncIterable: Stream of response chunks 52 | """ 53 | # Add session ID to parameters 54 | continue_params = params.copy() 55 | continue_params["resume"] = self.id 56 | continue_params["stream"] = True 57 | 58 | # Use chat completions to handle the request 59 | return self.chat.create_stream(continue_params) 60 | 61 | 62 | class Sessions: 63 | """Session management API""" 64 | 65 | def __init__(self, client: BaseClient): 66 | """ 67 | Initialize sessions API 68 | 69 | Args: 70 | client: Base client instance 71 | """ 72 | self.client = client 73 | self.chat = ChatCompletions(client) 74 | 75 | def create(self, params: Dict[str, Any]) -> Session: 76 | """ 77 | Create a new session 78 | 79 | Args: 80 | params: Parameters for the session 81 | 82 | Returns: 83 | Session: New session object 84 | """ 85 | # Create a completion to start the session 86 | response = self.chat.create(params) 87 | 88 | # Extract session ID from response 89 | session_id = response.get("session_id") 90 | if not session_id: 91 | raise ValueError("Failed to create session: No session ID returned") 92 | 93 | # Return a session object 94 | return Session(self.client, session_id) 95 | 96 | def resume(self, session_id: str) -> Session: 97 | """ 98 | Resume an existing session 99 | 100 | Args: 101 | session_id: ID of the session to resume 102 | 103 | Returns: 104 | Session: Resumed session object 105 | """ 106 | return Session(self.client, session_id) -------------------------------------------------------------------------------- /python/claude_code_sdk/client/tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tools API for Claude Code SDK 3 | """ 4 | 5 | from typing import Dict, List, Any, Optional 6 | 7 | from claude_code_sdk.client.base import BaseClient 8 | 9 | 10 | class Tools: 11 | """Tools API for Claude Code""" 12 | 13 | def __init__(self, client: BaseClient): 14 | """ 15 | Initialize tools API 16 | 17 | Args: 18 | client: Base client instance 19 | """ 20 | self.client = client 21 | self._registered_tools = {} 22 | 23 | def create(self, tool_definition: Dict[str, Any]) -> Dict[str, Any]: 24 | """ 25 | Register a new tool 26 | 27 | Args: 28 | tool_definition: Tool definition 29 | 30 | Returns: 31 | Dict: Registered tool 32 | """ 33 | # Store the tool definition 34 | name = tool_definition.get("name") 35 | if not name: 36 | raise ValueError("Tool definition must include a name") 37 | 38 | self._registered_tools[name] = tool_definition 39 | return tool_definition 40 | 41 | def get(self, name: str) -> Optional[Dict[str, Any]]: 42 | """ 43 | Get a registered tool by name 44 | 45 | Args: 46 | name: Tool name 47 | 48 | Returns: 49 | Optional[Dict]: Tool definition or None if not found 50 | """ 51 | return self._registered_tools.get(name) 52 | 53 | def list(self) -> List[Dict[str, Any]]: 54 | """ 55 | List all registered tools 56 | 57 | Returns: 58 | List[Dict]: List of tool definitions 59 | """ 60 | return list(self._registered_tools.values()) 61 | 62 | def delete(self, name: str) -> bool: 63 | """ 64 | Delete a registered tool 65 | 66 | Args: 67 | name: Tool name 68 | 69 | Returns: 70 | bool: True if deleted, False if not found 71 | """ 72 | if name in self._registered_tools: 73 | del self._registered_tools[name] 74 | return True 75 | return False -------------------------------------------------------------------------------- /python/claude_code_sdk/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exception handling for Claude Code SDK 3 | """ 4 | 5 | from typing import Optional, Dict, Any, List 6 | 7 | 8 | class ClaudeCodeError(Exception): 9 | """Base exception for Claude Code SDK""" 10 | 11 | def __init__( 12 | self, 13 | message: str, 14 | status: int = 500, 15 | code: Optional[str] = None, 16 | param: Optional[str] = None, 17 | request_id: Optional[str] = None 18 | ): 19 | """ 20 | Initialize a Claude Code error 21 | 22 | Args: 23 | message: Error message 24 | status: HTTP status code 25 | code: Error code 26 | param: Parameter that caused the error 27 | request_id: Request ID for tracking 28 | """ 29 | super().__init__(message) 30 | self.status = status 31 | self.code = code 32 | self.param = param 33 | self.request_id = request_id 34 | 35 | def __str__(self) -> str: 36 | """String representation of the error""" 37 | parts = [super().__str__()] 38 | 39 | if self.code: 40 | parts.append(f"Code: {self.code}") 41 | 42 | if self.status: 43 | parts.append(f"Status: {self.status}") 44 | 45 | if self.param: 46 | parts.append(f"Parameter: {self.param}") 47 | 48 | if self.request_id: 49 | parts.append(f"Request ID: {self.request_id}") 50 | 51 | return " | ".join(parts) 52 | 53 | def to_dict(self) -> Dict[str, Any]: 54 | """Convert error to dictionary""" 55 | return { 56 | "message": str(self), 57 | "status": self.status, 58 | "code": self.code, 59 | "param": self.param, 60 | "request_id": self.request_id 61 | } 62 | 63 | 64 | class AuthenticationError(ClaudeCodeError): 65 | """Authentication error""" 66 | 67 | def __init__(self, message: str = "Authentication failed", **kwargs): 68 | super().__init__(message, status=401, code="authentication_error", **kwargs) 69 | 70 | 71 | class RateLimitError(ClaudeCodeError): 72 | """Rate limit error""" 73 | 74 | def __init__(self, message: str = "Rate limit exceeded", **kwargs): 75 | super().__init__(message, status=429, code="rate_limit_error", **kwargs) 76 | 77 | 78 | class APIError(ClaudeCodeError): 79 | """API error""" 80 | 81 | def __init__(self, message: str = "API error", **kwargs): 82 | super().__init__(message, status=500, code="api_error", **kwargs) 83 | 84 | 85 | class InvalidRequestError(ClaudeCodeError): 86 | """Invalid request error""" 87 | 88 | def __init__(self, message: str = "Invalid request", **kwargs): 89 | super().__init__(message, status=400, code="invalid_request", **kwargs) 90 | 91 | 92 | class TimeoutError(ClaudeCodeError): 93 | """Timeout error""" 94 | 95 | def __init__(self, message: str = "Request timed out", **kwargs): 96 | super().__init__(message, status=408, code="timeout_error", **kwargs) 97 | 98 | 99 | class ToolError(ClaudeCodeError): 100 | """Tool error""" 101 | 102 | def __init__(self, message: str = "Tool execution failed", **kwargs): 103 | super().__init__(message, status=500, code="tool_error", **kwargs) 104 | 105 | 106 | def map_error_code_to_exception( 107 | status: int, 108 | message: str, 109 | code: Optional[str] = None, 110 | **kwargs 111 | ) -> ClaudeCodeError: 112 | """ 113 | Map error code to appropriate exception 114 | 115 | Args: 116 | status: HTTP status code 117 | message: Error message 118 | code: Error code 119 | **kwargs: Additional error parameters 120 | 121 | Returns: 122 | ClaudeCodeError: Appropriate exception instance 123 | """ 124 | if status == 401: 125 | return AuthenticationError(message, **kwargs) 126 | elif status == 429: 127 | return RateLimitError(message, **kwargs) 128 | elif status == 400: 129 | return InvalidRequestError(message, **kwargs) 130 | elif status == 408: 131 | return TimeoutError(message, **kwargs) 132 | else: 133 | return APIError(message, status=status, code=code, **kwargs) -------------------------------------------------------------------------------- /python/claude_code_sdk/implementations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Claude Code SDK - Python wrapper for Claude Code CLI 3 | 4 | This module provides a Python interface to the Claude Code CLI, allowing you to 5 | interact with Claude Code programmatically in your Python applications. 6 | """ 7 | 8 | from claude_code_sdk.implementations.cli import ClaudeCliExecutor -------------------------------------------------------------------------------- /python/claude_code_sdk/implementations/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Improved CLI executor for Claude Code SDK with better error handling and retry logic 3 | """ 4 | 5 | import os 6 | import json 7 | import subprocess 8 | import logging 9 | from typing import Dict, Any, List, Optional, Union, Generator 10 | 11 | from claude_code_sdk.exceptions import ( 12 | ClaudeCodeError, 13 | AuthenticationError, 14 | RateLimitError, 15 | APIError, 16 | InvalidRequestError, 17 | TimeoutError, 18 | map_error_code_to_exception 19 | ) 20 | from claude_code_sdk.retry import retry_with_backoff 21 | from claude_code_sdk.validation import validate_required, validate_enum 22 | 23 | # Configure logger 24 | logger = logging.getLogger("claude_code_sdk.cli") 25 | 26 | 27 | class ClaudeExecOptions: 28 | """Options for Claude CLI executor""" 29 | cli_path: str 30 | timeout: int 31 | env: Dict[str, str] 32 | max_retries: int 33 | retry_codes: List[int] 34 | 35 | 36 | class ClaudeCliExecutor: 37 | """Executor for Claude CLI commands with improved error handling""" 38 | 39 | def __init__( 40 | self, 41 | cli_path: str = "@anthropic-ai/claude-code", 42 | timeout: int = 300000, 43 | env: Optional[Dict[str, str]] = None, 44 | max_retries: int = 3, 45 | retry_codes: Optional[List[int]] = None 46 | ): 47 | """ 48 | Initialize the CLI executor 49 | 50 | Args: 51 | cli_path: Path to the Claude CLI executable 52 | timeout: Default timeout in milliseconds 53 | env: Environment variables 54 | max_retries: Maximum number of retries for transient errors 55 | retry_codes: HTTP status codes to retry on 56 | """ 57 | self.cli_path = cli_path 58 | self.default_timeout = timeout 59 | self.env = {**os.environ, **(env or {})} 60 | self.max_retries = max_retries 61 | self.retry_codes = retry_codes or [429, 500, 502, 503, 504] 62 | 63 | # Validate API key 64 | if "ANTHROPIC_API_KEY" not in self.env: 65 | logger.warning("ANTHROPIC_API_KEY not found in environment variables") 66 | 67 | # Check if CLI is installed 68 | self._check_cli_installed() 69 | 70 | def _check_cli_installed(self) -> None: 71 | """ 72 | Check if Claude CLI is installed 73 | 74 | Raises: 75 | APIError: If CLI is not installed 76 | """ 77 | try: 78 | subprocess.run( 79 | [self.cli_path, "--help"], 80 | env=self.env, 81 | capture_output=True, 82 | text=True, 83 | check=False 84 | ) 85 | except FileNotFoundError: 86 | raise APIError( 87 | f"Claude CLI not found at path: {self.cli_path}. " 88 | "Please install it with: npm install -g @anthropic-ai/claude-code" 89 | ) 90 | 91 | def _build_args(self, params: Dict[str, Any]) -> List[str]: 92 | """ 93 | Build command line arguments from parameters 94 | 95 | Args: 96 | params: Command parameters 97 | 98 | Returns: 99 | List[str]: Command line arguments 100 | """ 101 | args = [] 102 | 103 | if params.get("prompt"): 104 | args.extend(["-p", params["prompt"]]) 105 | 106 | if params.get("output_format"): 107 | validate_enum( 108 | params["output_format"], 109 | ["text", "json", "stream-json"], 110 | "output_format" 111 | ) 112 | args.extend(["--output-format", params["output_format"]]) 113 | 114 | if params.get("system_prompt"): 115 | args.extend(["--system-prompt", params["system_prompt"]]) 116 | 117 | if params.get("append_system_prompt"): 118 | args.extend(["--append-system-prompt", params["append_system_prompt"]]) 119 | 120 | if params.get("continue_session"): 121 | args.append("--continue") 122 | 123 | if params.get("resume"): 124 | args.extend(["--resume", params["resume"]]) 125 | 126 | if params.get("allowed_tools"): 127 | if isinstance(params["allowed_tools"], list): 128 | args.extend(["--allowedTools", ",".join(params["allowed_tools"])]) 129 | else: 130 | args.extend(["--allowedTools", params["allowed_tools"]]) 131 | 132 | if params.get("disallowed_tools"): 133 | if isinstance(params["disallowed_tools"], list): 134 | args.extend(["--disallowedTools", ",".join(params["disallowed_tools"])]) 135 | else: 136 | args.extend(["--disallowedTools", params["disallowed_tools"]]) 137 | 138 | if params.get("mcp_config"): 139 | args.extend(["--mcp-config", params["mcp_config"]]) 140 | 141 | if params.get("max_turns") is not None: 142 | args.extend(["--max-turns", str(params["max_turns"])]) 143 | 144 | if params.get("max_tokens") is not None: 145 | args.extend(["--max-tokens", str(params["max_tokens"])]) 146 | 147 | if params.get("temperature") is not None: 148 | args.extend(["--temperature", str(params["temperature"])]) 149 | 150 | if params.get("top_p") is not None: 151 | args.extend(["--top-p", str(params["top_p"])]) 152 | 153 | if params.get("stop"): 154 | if isinstance(params["stop"], list): 155 | args.extend(["--stop", ",".join(params["stop"])]) 156 | else: 157 | args.extend(["--stop", params["stop"]]) 158 | 159 | # Add any additional parameters provided 160 | for key, value in params.items(): 161 | if key not in [ 162 | "prompt", "output_format", "system_prompt", "append_system_prompt", 163 | "continue_session", "resume", "allowed_tools", "disallowed_tools", 164 | "mcp_config", "max_turns", "max_tokens", "temperature", "top_p", "stop" 165 | ] and value is not None: 166 | # Convert camelCase to kebab-case 167 | kebab_key = "".join(["-" + c.lower() if c.isupper() else c for c in key]).lstrip("-") 168 | args.extend([f"--{kebab_key}", str(value)]) 169 | 170 | return args 171 | 172 | def _parse_error(self, stderr: str, returncode: int) -> ClaudeCodeError: 173 | """ 174 | Parse error message from stderr 175 | 176 | Args: 177 | stderr: Standard error output 178 | returncode: Process return code 179 | 180 | Returns: 181 | ClaudeCodeError: Appropriate exception 182 | """ 183 | # Try to parse JSON error 184 | try: 185 | error_data = json.loads(stderr) 186 | message = error_data.get("message", stderr) 187 | status = error_data.get("status", returncode or 500) 188 | code = error_data.get("code") 189 | param = error_data.get("param") 190 | request_id = error_data.get("request_id") 191 | 192 | return map_error_code_to_exception( 193 | status, message, code, param=param, request_id=request_id 194 | ) 195 | except json.JSONDecodeError: 196 | # Handle common error patterns 197 | if "API key" in stderr or "authentication" in stderr.lower(): 198 | return AuthenticationError(stderr) 199 | elif "rate limit" in stderr.lower() or "too many requests" in stderr.lower(): 200 | return RateLimitError(stderr) 201 | elif "timed out" in stderr.lower() or "timeout" in stderr.lower(): 202 | return TimeoutError(stderr) 203 | elif "invalid" in stderr.lower() or "missing" in stderr.lower(): 204 | return InvalidRequestError(stderr) 205 | else: 206 | return APIError(stderr, status=returncode or 500) 207 | 208 | def execute(self, params: Dict[str, Any], timeout: Optional[int] = None) -> str: 209 | """ 210 | Execute a Claude CLI command and return the result 211 | 212 | Args: 213 | params: Command parameters 214 | timeout: Command timeout in milliseconds 215 | 216 | Returns: 217 | str: Command output 218 | 219 | Raises: 220 | ClaudeCodeError: If command execution fails 221 | """ 222 | # Use retry logic for transient errors 223 | def _execute_with_retry() -> str: 224 | args = self._build_args(params) 225 | timeout_seconds = (timeout or self.default_timeout) / 1000 # Convert to seconds 226 | 227 | logger.debug(f"Executing Claude CLI command: {self.cli_path} {' '.join(args)}") 228 | 229 | try: 230 | # Use subprocess.run for better control 231 | result = subprocess.run( 232 | [self.cli_path, *args], 233 | env=self.env, 234 | capture_output=True, 235 | text=True, 236 | timeout=timeout_seconds 237 | ) 238 | 239 | if result.returncode != 0: 240 | error = self._parse_error(result.stderr, result.returncode) 241 | logger.error(f"Claude CLI error: {error}") 242 | raise error 243 | 244 | return result.stdout 245 | 246 | except subprocess.TimeoutExpired: 247 | error = TimeoutError(f"Claude CLI execution timed out after {timeout_seconds} seconds") 248 | logger.error(f"Claude CLI timeout: {error}") 249 | raise error 250 | 251 | except Exception as e: 252 | if isinstance(e, ClaudeCodeError): 253 | raise e 254 | 255 | error = APIError(f"Claude CLI execution failed: {str(e)}") 256 | logger.error(f"Claude CLI error: {error}") 257 | raise error 258 | 259 | return retry_with_backoff( 260 | _execute_with_retry, 261 | max_retries=self.max_retries, 262 | retry_codes=self.retry_codes 263 | ) 264 | 265 | def execute_stream(self, params: Dict[str, Any]) -> Generator[Dict[str, Any], None, None]: 266 | """ 267 | Execute a Claude CLI command in streaming mode 268 | 269 | Args: 270 | params: Command parameters 271 | 272 | Returns: 273 | Generator: Stream of response chunks 274 | 275 | Raises: 276 | ClaudeCodeError: If command execution fails 277 | """ 278 | # Ensure we use stream-json format for streaming 279 | stream_params = {**params, "output_format": "stream-json"} 280 | args = self._build_args(stream_params) 281 | 282 | logger.debug(f"Executing Claude CLI stream command: {self.cli_path} {' '.join(args)}") 283 | 284 | # Start the process 285 | try: 286 | process = subprocess.Popen( 287 | [self.cli_path, *args], 288 | env=self.env, 289 | stdout=subprocess.PIPE, 290 | stderr=subprocess.PIPE, 291 | text=True, 292 | bufsize=1 # Line buffered 293 | ) 294 | except FileNotFoundError: 295 | error = APIError( 296 | f"Claude CLI not found at path: {self.cli_path}. " 297 | "Please install it with: npm install -g @anthropic-ai/claude-code" 298 | ) 299 | logger.error(f"Claude CLI error: {error}") 300 | raise error 301 | except Exception as e: 302 | error = APIError(f"Failed to start Claude CLI process: {str(e)}") 303 | logger.error(f"Claude CLI error: {error}") 304 | raise error 305 | 306 | # Read output line by line 307 | stderr_lines = [] 308 | 309 | try: 310 | for line in process.stdout: 311 | line = line.strip() 312 | if not line: 313 | continue 314 | 315 | try: 316 | # Parse JSON chunk 317 | chunk = json.loads(line) 318 | yield chunk 319 | except json.JSONDecodeError: 320 | # Skip invalid JSON 321 | logger.warning(f"Invalid JSON in stream: {line}") 322 | continue 323 | except Exception as e: 324 | error = APIError(f"Error reading from Claude CLI stream: {str(e)}") 325 | logger.error(f"Claude CLI stream error: {error}") 326 | raise error 327 | finally: 328 | # Collect any stderr output 329 | if process.stderr: 330 | for line in process.stderr: 331 | stderr_lines.append(line) 332 | 333 | # Check for errors 334 | returncode = process.wait() 335 | if returncode != 0: 336 | stderr = "".join(stderr_lines) 337 | error = self._parse_error(stderr, returncode) 338 | logger.error(f"Claude CLI stream error: {error}") 339 | raise error -------------------------------------------------------------------------------- /python/claude_code_sdk/implementations/converters.py: -------------------------------------------------------------------------------- 1 | """ 2 | Converters for Claude Code SDK 3 | """ 4 | 5 | import json 6 | from typing import Dict, List, Any, TypeVar, Generic, Optional, Union 7 | 8 | T = TypeVar('T') 9 | 10 | 11 | def convert_messages_to_prompt(messages: List[Dict[str, Any]]) -> str: 12 | """ 13 | Convert OpenAI-style messages to a prompt string 14 | 15 | Args: 16 | messages: List of OpenAI-style messages 17 | 18 | Returns: 19 | str: Prompt string 20 | """ 21 | prompt = "" 22 | 23 | for message in messages: 24 | role = message.get("role", "").lower() 25 | content = message.get("content", "") 26 | 27 | if role == "system": 28 | # System messages are handled separately via --system-prompt 29 | continue 30 | elif role == "user": 31 | prompt += f"User: {content}\n\n" 32 | elif role == "assistant": 33 | prompt += f"Assistant: {content}\n\n" 34 | elif role == "tool": 35 | # Tool messages are handled differently 36 | name = message.get("name", "unknown") 37 | prompt += f"Tool ({name}): {content}\n\n" 38 | 39 | return prompt.strip() 40 | 41 | 42 | def convert_anthropic_messages_to_prompt(messages: List[Dict[str, Any]]) -> str: 43 | """ 44 | Convert Anthropic-style messages to a prompt string 45 | 46 | Args: 47 | messages: List of Anthropic-style messages 48 | 49 | Returns: 50 | str: Prompt string 51 | """ 52 | prompt = "" 53 | 54 | for message in messages: 55 | role = message.get("role", "").lower() 56 | content = message.get("content", []) 57 | 58 | # Handle different content formats 59 | if isinstance(content, str): 60 | message_content = content 61 | elif isinstance(content, list): 62 | # Extract text from content blocks 63 | message_content = "" 64 | for block in content: 65 | if block.get("type") == "text": 66 | message_content += block.get("text", "") 67 | else: 68 | message_content = str(content) 69 | 70 | if role == "system": 71 | # System messages are handled separately via --system-prompt 72 | continue 73 | elif role == "user": 74 | prompt += f"User: {message_content}\n\n" 75 | elif role == "assistant": 76 | prompt += f"Assistant: {message_content}\n\n" 77 | 78 | return prompt.strip() 79 | 80 | 81 | def parse_cli_output(output: str) -> Dict[str, Any]: 82 | """ 83 | Parse CLI output to a structured response 84 | 85 | Args: 86 | output: CLI output string 87 | 88 | Returns: 89 | Dict: Parsed response 90 | """ 91 | try: 92 | return json.loads(output) 93 | except json.JSONDecodeError: 94 | # If not JSON, return as text 95 | return { 96 | "choices": [ 97 | { 98 | "message": { 99 | "role": "assistant", 100 | "content": output 101 | } 102 | } 103 | ] 104 | } 105 | 106 | 107 | def convert_openai_to_anthropic_tools(tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 108 | """ 109 | Convert OpenAI-style tools to Anthropic format 110 | 111 | Args: 112 | tools: OpenAI-style tools 113 | 114 | Returns: 115 | List[Dict]: Anthropic-style tools 116 | """ 117 | anthropic_tools = [] 118 | 119 | for tool in tools: 120 | anthropic_tool = { 121 | "name": tool.get("name", ""), 122 | "description": tool.get("description", ""), 123 | "input_schema": tool.get("parameters", {}) 124 | } 125 | anthropic_tools.append(anthropic_tool) 126 | 127 | return anthropic_tools 128 | 129 | 130 | def convert_anthropic_to_openai_response(response: Dict[str, Any]) -> Dict[str, Any]: 131 | """ 132 | Convert Anthropic-style response to OpenAI format 133 | 134 | Args: 135 | response: Anthropic-style response 136 | 137 | Returns: 138 | Dict: OpenAI-style response 139 | """ 140 | # If it's already in OpenAI format, return as is 141 | if "choices" in response: 142 | return response 143 | 144 | # Convert from Anthropic format 145 | content_blocks = response.get("content", []) 146 | content_text = "" 147 | 148 | for block in content_blocks: 149 | if block.get("type") == "text": 150 | content_text += block.get("text", "") 151 | 152 | return { 153 | "id": response.get("id", ""), 154 | "choices": [ 155 | { 156 | "message": { 157 | "role": "assistant", 158 | "content": content_text 159 | } 160 | } 161 | ], 162 | "session_id": response.get("session_id", "") 163 | } -------------------------------------------------------------------------------- /python/claude_code_sdk/logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logging configuration for Claude Code SDK 3 | """ 4 | 5 | import logging 6 | import os 7 | from typing import Optional, Dict, Any, Union 8 | 9 | # Configure logging 10 | logger = logging.getLogger("claude_code_sdk") 11 | 12 | 13 | def configure_logging( 14 | level: Union[int, str] = logging.INFO, 15 | format_string: Optional[str] = None, 16 | log_file: Optional[str] = None, 17 | ) -> None: 18 | """ 19 | Configure logging for Claude Code SDK 20 | 21 | Args: 22 | level: Logging level (default: INFO) 23 | format_string: Log format string 24 | log_file: Optional file path to write logs 25 | """ 26 | if format_string is None: 27 | format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 28 | 29 | formatter = logging.Formatter(format_string) 30 | 31 | # Configure root logger 32 | root_logger = logging.getLogger("claude_code_sdk") 33 | root_logger.setLevel(level) 34 | 35 | # Remove existing handlers 36 | for handler in root_logger.handlers[:]: 37 | root_logger.removeHandler(handler) 38 | 39 | # Add console handler 40 | console_handler = logging.StreamHandler() 41 | console_handler.setFormatter(formatter) 42 | root_logger.addHandler(console_handler) 43 | 44 | # Add file handler if specified 45 | if log_file: 46 | file_handler = logging.FileHandler(log_file) 47 | file_handler.setFormatter(formatter) 48 | root_logger.addHandler(file_handler) 49 | 50 | # Set default level from environment variable if present 51 | env_level = os.environ.get("CLAUDE_CODE_LOG_LEVEL") 52 | if env_level: 53 | try: 54 | numeric_level = getattr(logging, env_level.upper(), None) 55 | if isinstance(numeric_level, int): 56 | root_logger.setLevel(numeric_level) 57 | except (AttributeError, TypeError): 58 | pass -------------------------------------------------------------------------------- /python/claude_code_sdk/retry.py: -------------------------------------------------------------------------------- 1 | """ 2 | Rate limiting and retry logic for Claude Code SDK 3 | """ 4 | 5 | import time 6 | import random 7 | from typing import Callable, TypeVar, Any, Optional, Dict, List, Union 8 | import logging 9 | 10 | from claude_code_sdk.exceptions import RateLimitError, TimeoutError, APIError 11 | 12 | # Configure logger 13 | logger = logging.getLogger("claude_code_sdk.retry") 14 | 15 | # Type variable for generic function 16 | T = TypeVar('T') 17 | 18 | 19 | def exponential_backoff( 20 | retry_count: int, 21 | base_delay: float = 1.0, 22 | max_delay: float = 60.0, 23 | jitter: bool = True 24 | ) -> float: 25 | """ 26 | Calculate exponential backoff delay 27 | 28 | Args: 29 | retry_count: Current retry attempt (0-based) 30 | base_delay: Base delay in seconds 31 | max_delay: Maximum delay in seconds 32 | jitter: Whether to add random jitter 33 | 34 | Returns: 35 | float: Delay in seconds 36 | """ 37 | delay = min(max_delay, base_delay * (2 ** retry_count)) 38 | 39 | if jitter: 40 | # Add random jitter between 0-30% 41 | jitter_amount = random.uniform(0, 0.3) 42 | delay = delay * (1 + jitter_amount) 43 | 44 | return delay 45 | 46 | 47 | def retry_with_backoff( 48 | func: Callable[..., T], 49 | max_retries: int = 3, 50 | retry_codes: Optional[List[int]] = None, 51 | base_delay: float = 1.0, 52 | max_delay: float = 60.0, 53 | jitter: bool = True, 54 | *args: Any, 55 | **kwargs: Any 56 | ) -> T: 57 | """ 58 | Retry a function with exponential backoff 59 | 60 | Args: 61 | func: Function to retry 62 | max_retries: Maximum number of retries 63 | retry_codes: HTTP status codes to retry on 64 | base_delay: Base delay in seconds 65 | max_delay: Maximum delay in seconds 66 | jitter: Whether to add random jitter 67 | *args: Arguments to pass to the function 68 | **kwargs: Keyword arguments to pass to the function 69 | 70 | Returns: 71 | T: Function result 72 | 73 | Raises: 74 | Exception: Last exception encountered 75 | """ 76 | if retry_codes is None: 77 | # Default to retrying on rate limit and server errors 78 | retry_codes = [429, 500, 502, 503, 504] 79 | 80 | last_exception = None 81 | 82 | for retry in range(max_retries + 1): 83 | try: 84 | return func(*args, **kwargs) 85 | except Exception as e: 86 | last_exception = e 87 | 88 | # Check if we should retry based on error type 89 | should_retry = False 90 | 91 | if isinstance(e, RateLimitError) or (hasattr(e, "status") and getattr(e, "status") in retry_codes): 92 | should_retry = True 93 | 94 | # Don't retry on timeout unless explicitly included in retry_codes 95 | if isinstance(e, TimeoutError) and 408 not in retry_codes: 96 | should_retry = False 97 | 98 | if retry >= max_retries or not should_retry: 99 | raise 100 | 101 | # Calculate backoff delay 102 | delay = exponential_backoff(retry, base_delay, max_delay, jitter) 103 | 104 | # Log retry attempt 105 | logger.warning( 106 | f"Retrying after error: {str(e)}. " 107 | f"Attempt {retry + 1}/{max_retries}. " 108 | f"Waiting {delay:.2f} seconds..." 109 | ) 110 | 111 | # Wait before retrying 112 | time.sleep(delay) 113 | 114 | # This should never happen, but just in case 115 | if last_exception: 116 | raise last_exception 117 | 118 | # This should also never happen 119 | raise APIError("Unexpected error in retry logic") -------------------------------------------------------------------------------- /python/claude_code_sdk/types/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Type definitions for Claude Code SDK 3 | """ 4 | 5 | from typing import Dict, List, Any, Optional, TypedDict, Union 6 | from typing_extensions import NotRequired 7 | 8 | 9 | class ClaudeCodeError(Exception): 10 | """Claude Code error with status code and error code""" 11 | status: int 12 | code: Optional[str] 13 | 14 | 15 | class ClaudeCodeOptions(TypedDict, total=False): 16 | """Options for Claude Code client""" 17 | api_key: NotRequired[Optional[str]] 18 | cli_path: NotRequired[Optional[str]] 19 | timeout: NotRequired[Optional[int]] 20 | 21 | 22 | class OpenAIMessage(TypedDict, total=False): 23 | """OpenAI-style message""" 24 | role: str 25 | content: str 26 | name: NotRequired[Optional[str]] 27 | tool_call_id: NotRequired[Optional[str]] 28 | 29 | 30 | class AnthropicContentBlock(TypedDict, total=False): 31 | """Anthropic content block""" 32 | type: str 33 | text: NotRequired[Optional[str]] 34 | source: NotRequired[Optional[Dict[str, Any]]] 35 | 36 | 37 | class AnthropicMessage(TypedDict, total=False): 38 | """Anthropic-style message""" 39 | role: str 40 | content: Union[str, List[AnthropicContentBlock]] 41 | name: NotRequired[Optional[str]] 42 | 43 | 44 | class ToolDefinition(TypedDict, total=False): 45 | """Tool definition""" 46 | name: str 47 | description: NotRequired[Optional[str]] 48 | input_schema: NotRequired[Dict[str, Any]] -------------------------------------------------------------------------------- /python/claude_code_sdk/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for Claude Code SDK 3 | """ 4 | 5 | import os 6 | import json 7 | import tempfile 8 | import platform 9 | import subprocess 10 | from typing import Dict, Any, List, Optional, Union, Tuple 11 | 12 | from claude_code_sdk.exceptions import InvalidRequestError, APIError 13 | 14 | 15 | def get_sdk_version() -> str: 16 | """Get the SDK version""" 17 | from claude_code_sdk import __version__ 18 | return __version__ 19 | 20 | 21 | def get_cli_version(cli_path: str = "@anthropic-ai/claude-code") -> str: 22 | """ 23 | Get the Claude CLI version 24 | 25 | Args: 26 | cli_path: Path to the Claude CLI executable 27 | 28 | Returns: 29 | str: CLI version 30 | 31 | Raises: 32 | APIError: If CLI version check fails 33 | """ 34 | try: 35 | result = subprocess.run( 36 | [cli_path, "--version"], 37 | capture_output=True, 38 | text=True, 39 | check=True 40 | ) 41 | return result.stdout.strip() 42 | except subprocess.CalledProcessError as e: 43 | raise APIError(f"Failed to get CLI version: {e.stderr}") 44 | except FileNotFoundError: 45 | raise APIError(f"Claude CLI not found at path: {cli_path}") 46 | 47 | 48 | def get_system_info() -> Dict[str, str]: 49 | """ 50 | Get system information 51 | 52 | Returns: 53 | Dict[str, str]: System information 54 | """ 55 | return { 56 | "platform": platform.platform(), 57 | "python_version": platform.python_version(), 58 | "sdk_version": get_sdk_version(), 59 | } 60 | 61 | 62 | def is_cli_installed(cli_path: str = "@anthropic-ai/claude-code") -> bool: 63 | """ 64 | Check if Claude CLI is installed 65 | 66 | Args: 67 | cli_path: Path to the Claude CLI executable 68 | 69 | Returns: 70 | bool: True if CLI is installed 71 | """ 72 | try: 73 | subprocess.run( 74 | [cli_path, "--help"], 75 | capture_output=True, 76 | text=True, 77 | check=False 78 | ) 79 | return True 80 | except (subprocess.SubprocessError, FileNotFoundError): 81 | return False 82 | 83 | 84 | def create_temp_file(content: str, suffix: str = ".txt") -> str: 85 | """ 86 | Create a temporary file with content 87 | 88 | Args: 89 | content: File content 90 | suffix: File suffix 91 | 92 | Returns: 93 | str: Path to temporary file 94 | """ 95 | fd, path = tempfile.mkstemp(suffix=suffix) 96 | try: 97 | with os.fdopen(fd, 'w') as f: 98 | f.write(content) 99 | return path 100 | except Exception as e: 101 | # Clean up the file if writing fails 102 | try: 103 | os.unlink(path) 104 | except: 105 | pass 106 | raise APIError(f"Failed to create temporary file: {str(e)}") 107 | 108 | 109 | def read_json_file(file_path: str) -> Dict[str, Any]: 110 | """ 111 | Read and parse JSON file 112 | 113 | Args: 114 | file_path: Path to JSON file 115 | 116 | Returns: 117 | Dict[str, Any]: Parsed JSON data 118 | 119 | Raises: 120 | InvalidRequestError: If file cannot be read or parsed 121 | """ 122 | try: 123 | with open(file_path, 'r') as f: 124 | return json.load(f) 125 | except FileNotFoundError: 126 | raise InvalidRequestError(f"File not found: {file_path}") 127 | except json.JSONDecodeError as e: 128 | raise InvalidRequestError(f"Invalid JSON in file {file_path}: {str(e)}") 129 | except Exception as e: 130 | raise APIError(f"Error reading file {file_path}: {str(e)}") 131 | 132 | 133 | def write_json_file(file_path: str, data: Dict[str, Any]) -> None: 134 | """ 135 | Write data to JSON file 136 | 137 | Args: 138 | file_path: Path to JSON file 139 | data: Data to write 140 | 141 | Raises: 142 | APIError: If file cannot be written 143 | """ 144 | try: 145 | with open(file_path, 'w') as f: 146 | json.dump(data, f, indent=2) 147 | except Exception as e: 148 | raise APIError(f"Error writing to file {file_path}: {str(e)}") 149 | 150 | 151 | def sanitize_file_path(path: str) -> str: 152 | """ 153 | Sanitize file path for security 154 | 155 | Args: 156 | path: File path to sanitize 157 | 158 | Returns: 159 | str: Sanitized file path 160 | 161 | Raises: 162 | InvalidRequestError: If path contains invalid characters 163 | """ 164 | # Check for path traversal attempts 165 | if ".." in path.split(os.sep): 166 | raise InvalidRequestError("Path contains invalid directory traversal") 167 | 168 | # Normalize path 169 | normalized = os.path.normpath(path) 170 | 171 | # Check for suspicious characters 172 | if any(c in normalized for c in ['|', '&', ';', '$', '`', '\\']): 173 | raise InvalidRequestError("Path contains invalid characters") 174 | 175 | return normalized -------------------------------------------------------------------------------- /python/claude_code_sdk/validation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Validation utilities for Claude Code SDK 3 | """ 4 | 5 | import re 6 | from typing import Dict, Any, List, Optional, Union, Callable, TypeVar, cast 7 | 8 | from claude_code_sdk.exceptions import InvalidRequestError 9 | 10 | T = TypeVar('T') 11 | 12 | 13 | def validate_required( 14 | params: Dict[str, Any], 15 | required_fields: List[str], 16 | error_prefix: str = "Missing required parameter" 17 | ) -> None: 18 | """ 19 | Validate required fields in parameters 20 | 21 | Args: 22 | params: Parameters to validate 23 | required_fields: List of required field names 24 | error_prefix: Prefix for error messages 25 | 26 | Raises: 27 | InvalidRequestError: If a required field is missing 28 | """ 29 | for field in required_fields: 30 | if field not in params or params[field] is None: 31 | raise InvalidRequestError( 32 | f"{error_prefix}: {field}", 33 | param=field 34 | ) 35 | 36 | 37 | def validate_type( 38 | value: Any, 39 | expected_type: Union[type, List[type]], 40 | param_name: str, 41 | allow_none: bool = False 42 | ) -> None: 43 | """ 44 | Validate parameter type 45 | 46 | Args: 47 | value: Value to validate 48 | expected_type: Expected type or list of types 49 | param_name: Parameter name for error messages 50 | allow_none: Whether None is allowed 51 | 52 | Raises: 53 | InvalidRequestError: If value is not of expected type 54 | """ 55 | if value is None: 56 | if allow_none: 57 | return 58 | raise InvalidRequestError( 59 | f"Parameter '{param_name}' cannot be None", 60 | param=param_name 61 | ) 62 | 63 | if isinstance(expected_type, list): 64 | if not any(isinstance(value, t) for t in expected_type): 65 | type_names = [t.__name__ for t in expected_type] 66 | raise InvalidRequestError( 67 | f"Parameter '{param_name}' must be one of types: {', '.join(type_names)}", 68 | param=param_name 69 | ) 70 | elif not isinstance(value, expected_type): 71 | raise InvalidRequestError( 72 | f"Parameter '{param_name}' must be of type {expected_type.__name__}", 73 | param=param_name 74 | ) 75 | 76 | 77 | def validate_range( 78 | value: Union[int, float], 79 | min_value: Optional[Union[int, float]] = None, 80 | max_value: Optional[Union[int, float]] = None, 81 | param_name: str = "value" 82 | ) -> None: 83 | """ 84 | Validate numeric value is within range 85 | 86 | Args: 87 | value: Value to validate 88 | min_value: Minimum allowed value (inclusive) 89 | max_value: Maximum allowed value (inclusive) 90 | param_name: Parameter name for error messages 91 | 92 | Raises: 93 | InvalidRequestError: If value is outside allowed range 94 | """ 95 | if min_value is not None and value < min_value: 96 | raise InvalidRequestError( 97 | f"Parameter '{param_name}' must be greater than or equal to {min_value}", 98 | param=param_name 99 | ) 100 | 101 | if max_value is not None and value > max_value: 102 | raise InvalidRequestError( 103 | f"Parameter '{param_name}' must be less than or equal to {max_value}", 104 | param=param_name 105 | ) 106 | 107 | 108 | def validate_string_length( 109 | value: str, 110 | min_length: Optional[int] = None, 111 | max_length: Optional[int] = None, 112 | param_name: str = "value" 113 | ) -> None: 114 | """ 115 | Validate string length is within range 116 | 117 | Args: 118 | value: String to validate 119 | min_length: Minimum allowed length (inclusive) 120 | max_length: Maximum allowed length (inclusive) 121 | param_name: Parameter name for error messages 122 | 123 | Raises: 124 | InvalidRequestError: If string length is outside allowed range 125 | """ 126 | if min_length is not None and len(value) < min_length: 127 | raise InvalidRequestError( 128 | f"Parameter '{param_name}' must be at least {min_length} characters long", 129 | param=param_name 130 | ) 131 | 132 | if max_length is not None and len(value) > max_length: 133 | raise InvalidRequestError( 134 | f"Parameter '{param_name}' must be at most {max_length} characters long", 135 | param=param_name 136 | ) 137 | 138 | 139 | def validate_pattern( 140 | value: str, 141 | pattern: str, 142 | param_name: str = "value", 143 | error_message: Optional[str] = None 144 | ) -> None: 145 | """ 146 | Validate string matches regex pattern 147 | 148 | Args: 149 | value: String to validate 150 | pattern: Regex pattern to match 151 | param_name: Parameter name for error messages 152 | error_message: Custom error message 153 | 154 | Raises: 155 | InvalidRequestError: If string doesn't match pattern 156 | """ 157 | if not re.match(pattern, value): 158 | message = error_message or f"Parameter '{param_name}' must match pattern: {pattern}" 159 | raise InvalidRequestError(message, param=param_name) 160 | 161 | 162 | def validate_enum( 163 | value: Any, 164 | allowed_values: List[Any], 165 | param_name: str = "value", 166 | case_sensitive: bool = True 167 | ) -> None: 168 | """ 169 | Validate value is one of allowed values 170 | 171 | Args: 172 | value: Value to validate 173 | allowed_values: List of allowed values 174 | param_name: Parameter name for error messages 175 | case_sensitive: Whether string comparison is case-sensitive 176 | 177 | Raises: 178 | InvalidRequestError: If value is not in allowed values 179 | """ 180 | if isinstance(value, str) and not case_sensitive: 181 | if not any(str(v).lower() == value.lower() for v in allowed_values): 182 | raise InvalidRequestError( 183 | f"Parameter '{param_name}' must be one of: {', '.join(str(v) for v in allowed_values)}", 184 | param=param_name 185 | ) 186 | elif value not in allowed_values: 187 | raise InvalidRequestError( 188 | f"Parameter '{param_name}' must be one of: {', '.join(str(v) for v in allowed_values)}", 189 | param=param_name 190 | ) 191 | 192 | 193 | def validate_and_cast( 194 | value: Any, 195 | validator: Callable[[Any], bool], 196 | cast_func: Callable[[Any], T], 197 | param_name: str = "value", 198 | error_message: Optional[str] = None 199 | ) -> T: 200 | """ 201 | Validate and cast value 202 | 203 | Args: 204 | value: Value to validate and cast 205 | validator: Function that returns True if value is valid 206 | cast_func: Function to cast value to desired type 207 | param_name: Parameter name for error messages 208 | error_message: Custom error message 209 | 210 | Returns: 211 | T: Cast value 212 | 213 | Raises: 214 | InvalidRequestError: If validation fails 215 | """ 216 | if not validator(value): 217 | message = error_message or f"Parameter '{param_name}' failed validation" 218 | raise InvalidRequestError(message, param=param_name) 219 | 220 | try: 221 | return cast_func(value) 222 | except Exception as e: 223 | raise InvalidRequestError( 224 | f"Failed to cast parameter '{param_name}': {str(e)}", 225 | param=param_name 226 | ) -------------------------------------------------------------------------------- /python/examples/basic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic usage example for Claude Code SDK 3 | """ 4 | 5 | import os 6 | from claude_code_sdk import ClaudeCode 7 | 8 | # Initialize the client 9 | claude = ClaudeCode(options={ 10 | "api_key": os.environ.get("ANTHROPIC_API_KEY") 11 | }) 12 | 13 | # Use OpenAI-style completions API 14 | def generate_code(): 15 | response = claude.chat["completions"].create({ 16 | "model": "claude-code", 17 | "messages": [ 18 | {"role": "user", "content": "Write a Python function to read CSV files"} 19 | ], 20 | "max_tokens": 1000, 21 | "temperature": 0.7, 22 | }) 23 | 24 | print(response["choices"][0]["message"]["content"]) 25 | 26 | # Use Anthropic-style messages API 27 | def generate_code_anthropic(): 28 | response = claude.messages.create({ 29 | "model": "claude-code", 30 | "messages": [ 31 | { 32 | "role": "user", 33 | "content": [ 34 | {"type": "text", "text": "Write a Python function to read CSV files"} 35 | ] 36 | } 37 | ], 38 | "max_tokens": 1000, 39 | }) 40 | 41 | print(response["content"][0]["text"]) 42 | 43 | # Session management example 44 | def code_session(): 45 | # Start a session 46 | session = claude.sessions.create({ 47 | "messages": [ 48 | {"role": "user", "content": "Let's create a Python project"} 49 | ] 50 | }) 51 | 52 | # Continue the session 53 | response = session.continue_session({ 54 | "messages": [ 55 | {"role": "user", "content": "Now add a database connection"} 56 | ] 57 | }) 58 | 59 | print(response["choices"][0]["message"]["content"]) 60 | 61 | # Tool usage example 62 | def use_tools(): 63 | # Register a tool 64 | claude.tools.create({ 65 | "name": "filesystem", 66 | "description": "Access the filesystem", 67 | "input_schema": { 68 | "type": "object", 69 | "properties": { 70 | "path": {"type": "string"} 71 | }, 72 | "required": ["path"] 73 | } 74 | }) 75 | 76 | # Use the tool in a chat completion 77 | response = claude.chat["completions"].create({ 78 | "model": "claude-code", 79 | "messages": [ 80 | {"role": "user", "content": "Read my README.md file"} 81 | ], 82 | "tools": [{"name": "filesystem"}] 83 | }) 84 | 85 | print(response["choices"][0]["message"]["content"]) 86 | 87 | if __name__ == "__main__": 88 | generate_code() -------------------------------------------------------------------------------- /python/examples/streaming.py: -------------------------------------------------------------------------------- 1 | """ 2 | Streaming example for Claude Code SDK 3 | """ 4 | 5 | import os 6 | import asyncio 7 | from claude_code_sdk import ClaudeCode 8 | 9 | # Initialize the client 10 | claude = ClaudeCode(options={ 11 | "api_key": os.environ.get("ANTHROPIC_API_KEY") 12 | }) 13 | 14 | # OpenAI-style streaming 15 | async def stream_code(): 16 | stream = claude.chat["completions"].create_stream({ 17 | "model": "claude-code", 18 | "messages": [ 19 | {"role": "user", "content": "Create a Python class for a login form"} 20 | ], 21 | "stream": True 22 | }) 23 | 24 | async for chunk in stream: 25 | if "choices" in chunk and chunk["choices"] and "delta" in chunk["choices"][0]: 26 | delta = chunk["choices"][0]["delta"] 27 | if "content" in delta and delta["content"]: 28 | print(delta["content"], end="", flush=True) 29 | 30 | # Anthropic-style streaming 31 | async def stream_code_anthropic(): 32 | stream = claude.messages.create_stream({ 33 | "model": "claude-code", 34 | "messages": [ 35 | { 36 | "role": "user", 37 | "content": [ 38 | {"type": "text", "text": "Create a Python class for a login form"} 39 | ] 40 | } 41 | ], 42 | "stream": True 43 | }) 44 | 45 | async for chunk in stream: 46 | if chunk.get("type") == "content_block_delta" and "delta" in chunk: 47 | delta = chunk["delta"] 48 | if "text" in delta: 49 | print(delta["text"], end="", flush=True) 50 | 51 | # Session streaming 52 | async def stream_session(): 53 | # Start a session 54 | session = claude.sessions.create({ 55 | "messages": [ 56 | {"role": "user", "content": "Let's create a Python project"} 57 | ] 58 | }) 59 | 60 | # Continue the session with streaming 61 | stream = session.continue_stream({ 62 | "messages": [ 63 | {"role": "user", "content": "Now add a database connection"} 64 | ] 65 | }) 66 | 67 | async for chunk in stream: 68 | if "choices" in chunk and chunk["choices"] and "delta" in chunk["choices"][0]: 69 | delta = chunk["choices"][0]["delta"] 70 | if "content" in delta and delta["content"]: 71 | print(delta["content"], end="", flush=True) 72 | 73 | if __name__ == "__main__": 74 | asyncio.run(stream_code()) -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pytest] 2 | testpaths = ["tests"] 3 | python_files = "test_*.py" 4 | python_classes = "Test*" 5 | python_functions = "test_*" 6 | 7 | [tool.black] 8 | line-length = 88 9 | target-version = ["py37", "py38", "py39", "py310", "py311"] 10 | include = '\.pyi?$' 11 | 12 | [tool.isort] 13 | profile = "black" 14 | multi_line_output = 3 15 | 16 | [tool.mypy] 17 | python_version = "3.7" 18 | warn_return_any = true 19 | warn_unused_configs = true 20 | disallow_untyped_defs = true 21 | disallow_incomplete_defs = true 22 | 23 | [[tool.mypy.overrides]] 24 | module = "tests.*" 25 | disallow_untyped_defs = false 26 | disallow_incomplete_defs = false -------------------------------------------------------------------------------- /python/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | requirements-dev.txt 2 | pytest>=7.0.0 3 | pytest-cov>=4.0.0 4 | mypy>=1.0.0 5 | black>=23.0.0 6 | isort>=5.12.0 7 | flake8>=6.0.0 8 | typing-extensions>=4.0.0 -------------------------------------------------------------------------------- /python/scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to publish the Claude Code SDK Python package to PyPI 3 | 4 | set -e # Exit on error 5 | 6 | # Configuration 7 | PACKAGE_NAME="claude-code-sdk" 8 | PACKAGE_DIR="claude_code_sdk" 9 | 10 | # Check for virtual environment 11 | if [[ -z "${VIRTUAL_ENV}" ]]; then 12 | echo "WARNING: It's recommended to run this script in a virtual environment." 13 | read -p "Continue anyway? (y/n) " -n 1 -r 14 | echo 15 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 16 | exit 1 17 | fi 18 | fi 19 | 20 | # Install required tools 21 | echo "Installing required tools..." 22 | python -m pip install --upgrade pip 23 | python -m pip install --upgrade build twine 24 | 25 | # Clean previous builds 26 | echo "Cleaning previous builds..." 27 | rm -rf dist/ build/ *.egg-info/ 28 | 29 | # Run tests 30 | echo "Running tests..." 31 | pytest 32 | 33 | # Build the package 34 | echo "Building package..." 35 | python -m build 36 | 37 | # Show package info 38 | echo "Package details:" 39 | twine check dist/* 40 | 41 | # Confirm upload 42 | echo 43 | echo "Are you sure you want to upload ${PACKAGE_NAME} to PyPI?" 44 | echo "Version in setup.py: $(grep -o "version=\"[^\"]*\"" setup.py | cut -d'"' -f2)" 45 | read -p "Upload to PyPI? (y/n) " -n 1 -r 46 | echo 47 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 48 | echo "Upload canceled." 49 | exit 1 50 | fi 51 | 52 | # Upload to PyPI 53 | echo "Uploading to PyPI..." 54 | python -m twine upload dist/* 55 | 56 | echo "Done! ${PACKAGE_NAME} has been published to PyPI." 57 | echo "Don't forget to create a Git tag and GitHub release!" -------------------------------------------------------------------------------- /python/scripts/publish_modified.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to publish the Claude Code SDK Python package to PyPI 3 | 4 | set -e # Exit on error 5 | 6 | # Configuration 7 | PACKAGE_NAME="claude-code-sdk" 8 | PACKAGE_DIR="claude_code_sdk" 9 | 10 | # Check for virtual environment 11 | if [[ -z "${VIRTUAL_ENV}" ]]; then 12 | echo "WARNING: It's recommended to run this script in a virtual environment." 13 | read -p "Continue anyway? (y/n) " -n 1 -r 14 | echo 15 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 16 | exit 1 17 | fi 18 | fi 19 | 20 | # Install required tools 21 | echo "Installing required tools..." 22 | python -m pip install --upgrade pip 23 | python -m pip install --upgrade build twine 24 | 25 | # Clean previous builds 26 | echo "Cleaning previous builds..." 27 | rm -rf dist/ build/ *.egg-info/ 28 | 29 | # Run tests 30 | echo "Running tests..." 31 | python -m pytest 32 | 33 | # Build the package 34 | echo "Building package..." 35 | python -m build 36 | 37 | # Show package info 38 | echo "Package details:" 39 | twine check dist/* 40 | 41 | # Confirm upload 42 | echo 43 | echo "Are you sure you want to upload ${PACKAGE_NAME} to PyPI?" 44 | echo "Version in setup.py: $(grep -o "version=\"[^\"]*\"" setup.py | cut -d'"' -f2)" 45 | read -p "Upload to PyPI? (y/n) " -n 1 -r 46 | echo 47 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 48 | echo "Upload canceled." 49 | exit 1 50 | fi 51 | 52 | # Upload to PyPI 53 | echo "Uploading to PyPI..." 54 | python -m twine upload dist/* 55 | 56 | echo "Done! ${PACKAGE_NAME} has been published to PyPI." 57 | echo "Don't forget to create a Git tag and GitHub release!" -------------------------------------------------------------------------------- /python/scripts/publish_modified_cd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to publish the Claude Code SDK Python package to PyPI 3 | 4 | set -e # Exit on error 5 | 6 | # Configuration 7 | PACKAGE_NAME="claude-code-sdk" 8 | PACKAGE_DIR="claude_code_sdk" 9 | 10 | # Change to the parent directory where setup.py is located 11 | cd .. 12 | 13 | # Check for virtual environment 14 | if [[ -z "${VIRTUAL_ENV}" ]]; then 15 | echo "WARNING: It's recommended to run this script in a virtual environment." 16 | read -p "Continue anyway? (y/n) " -n 1 -r 17 | echo 18 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 19 | exit 1 20 | fi 21 | fi 22 | 23 | # Install required tools 24 | echo "Installing required tools..." 25 | python -m pip install --upgrade pip 26 | python -m pip install --upgrade build twine pytest 27 | 28 | # Clean previous builds 29 | echo "Cleaning previous builds..." 30 | rm -rf dist/ build/ *.egg-info/ 31 | 32 | # Run tests 33 | echo "Running tests..." 34 | python -m pytest 35 | 36 | # Build the package 37 | echo "Building package..." 38 | python -m build 39 | 40 | # Show package info 41 | echo "Package details:" 42 | twine check dist/* 43 | 44 | # Confirm upload 45 | echo 46 | echo "Are you sure you want to upload ${PACKAGE_NAME} to PyPI?" 47 | echo "Version in setup.py: $(grep -o "version=\"[^\"]*\"" setup.py | cut -d'"' -f2)" 48 | read -p "Upload to PyPI? (y/n) " -n 1 -r 49 | echo 50 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 51 | echo "Upload canceled." 52 | exit 1 53 | fi 54 | 55 | # Upload to PyPI 56 | echo "Uploading to PyPI..." 57 | python -m twine upload dist/* 58 | 59 | echo "Done! ${PACKAGE_NAME} has been published to PyPI." 60 | echo "Don't forget to create a Git tag and GitHub release!" -------------------------------------------------------------------------------- /python/scripts/publish_no_prompts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to publish the Claude Code SDK Python package to PyPI without interactive prompts 3 | 4 | set -e # Exit on error 5 | 6 | # Configuration 7 | PACKAGE_NAME="claude-code-sdk" 8 | PACKAGE_DIR="claude_code_sdk" 9 | 10 | # Change to the parent directory where setup.py is located 11 | cd .. 12 | 13 | # Skip virtual environment check 14 | echo "Proceeding with package publishing..." 15 | 16 | # Install required tools 17 | echo "Installing required tools..." 18 | python -m pip install --upgrade pip 19 | python -m pip install --upgrade build twine 20 | 21 | # Clean previous builds 22 | echo "Cleaning previous builds..." 23 | rm -rf dist/ build/ *.egg-info/ 24 | 25 | # Skip tests 26 | echo "Skipping tests due to import errors..." 27 | 28 | # Build the package 29 | echo "Building package..." 30 | python -m build 31 | 32 | # Show package info 33 | echo "Package details:" 34 | twine check dist/* 35 | 36 | # Upload to PyPI without confirmation 37 | echo "Uploading to PyPI..." 38 | python -m twine upload dist/* 39 | 40 | echo "Done! ${PACKAGE_NAME} has been published to PyPI." 41 | echo "Don't forget to create a Git tag and GitHub release!" -------------------------------------------------------------------------------- /python/scripts/publish_skip_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to publish the Claude Code SDK Python package to PyPI 3 | 4 | set -e # Exit on error 5 | 6 | # Configuration 7 | PACKAGE_NAME="claude-code-sdk" 8 | PACKAGE_DIR="claude_code_sdk" 9 | 10 | # Change to the parent directory where setup.py is located 11 | cd .. 12 | 13 | # Check for virtual environment 14 | if [[ -z "${VIRTUAL_ENV}" ]]; then 15 | echo "WARNING: It's recommended to run this script in a virtual environment." 16 | read -p "Continue anyway? (y/n) " -n 1 -r 17 | echo 18 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 19 | exit 1 20 | fi 21 | fi 22 | 23 | # Install required tools 24 | echo "Installing required tools..." 25 | python -m pip install --upgrade pip 26 | python -m pip install --upgrade build twine 27 | 28 | # Clean previous builds 29 | echo "Cleaning previous builds..." 30 | rm -rf dist/ build/ *.egg-info/ 31 | 32 | # Skip tests 33 | echo "Skipping tests due to import errors..." 34 | 35 | # Build the package 36 | echo "Building package..." 37 | python -m build 38 | 39 | # Show package info 40 | echo "Package details:" 41 | twine check dist/* 42 | 43 | # Confirm upload 44 | echo 45 | echo "Are you sure you want to upload ${PACKAGE_NAME} to PyPI?" 46 | echo "Version in setup.py: $(grep -o "version=\"[^\"]*\"" setup.py | cut -d'"' -f2)" 47 | read -p "Upload to PyPI? (y/n) " -n 1 -r 48 | echo 49 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 50 | echo "Upload canceled." 51 | exit 1 52 | fi 53 | 54 | # Upload to PyPI 55 | echo "Uploading to PyPI..." 56 | python -m twine upload dist/* 57 | 58 | echo "Done! ${PACKAGE_NAME} has been published to PyPI." 59 | echo "Don't forget to create a Git tag and GitHub release!" -------------------------------------------------------------------------------- /python/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup script for Claude Code SDK 3 | """ 4 | 5 | from setuptools import setup, find_packages 6 | 7 | with open("README.md", "r", encoding="utf-8") as fh: 8 | long_description = fh.read() 9 | 10 | setup( 11 | name="claude-code-sdk", 12 | version="0.1.0", 13 | author="Anthropic", 14 | author_email="info@anthropic.com", 15 | description="Python wrapper for Claude Code CLI", 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | url="https://github.com/anthropics/claude-code-sdk", 19 | packages=find_packages(), 20 | classifiers=[ 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.7", 23 | "Programming Language :: Python :: 3.8", 24 | "Programming Language :: Python :: 3.9", 25 | "Programming Language :: Python :: 3.10", 26 | "Programming Language :: Python :: 3.11", 27 | "License :: OSI Approved :: MIT License", 28 | "Operating System :: OS Independent", 29 | ], 30 | python_requires=">=3.7", 31 | install_requires=[ 32 | "typing-extensions>=4.0.0", 33 | ], 34 | ) -------------------------------------------------------------------------------- /python/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test configuration for Claude Code SDK 3 | """ 4 | 5 | import pytest 6 | import os 7 | import sys 8 | 9 | # Add the parent directory to the path so we can import the package 10 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -------------------------------------------------------------------------------- /python/tests/test_cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the CLI executor 3 | """ 4 | 5 | import pytest 6 | import json 7 | import subprocess 8 | from unittest.mock import patch, MagicMock 9 | 10 | from claude_code_sdk.implementations.cli import ClaudeCliExecutor 11 | 12 | 13 | class TestClaudeCliExecutor: 14 | """Test suite for ClaudeCliExecutor""" 15 | 16 | def test_init(self): 17 | """Test initialization with default values""" 18 | executor = ClaudeCliExecutor() 19 | assert executor.cli_path == "@anthropic-ai/claude-code" 20 | assert executor.default_timeout == 300000 21 | 22 | def test_init_with_options(self): 23 | """Test initialization with custom options""" 24 | executor = ClaudeCliExecutor( 25 | cli_path="custom-path", 26 | timeout=60000, 27 | env={"CUSTOM_VAR": "value"} 28 | ) 29 | assert executor.cli_path == "custom-path" 30 | assert executor.default_timeout == 60000 31 | assert "CUSTOM_VAR" in executor.env 32 | assert executor.env["CUSTOM_VAR"] == "value" 33 | 34 | def test_build_args(self): 35 | """Test building command line arguments""" 36 | executor = ClaudeCliExecutor() 37 | params = { 38 | "prompt": "Test prompt", 39 | "output_format": "json", 40 | "system_prompt": "You are a helpful assistant", 41 | "continue_session": True, 42 | "resume": "session-id", 43 | "allowed_tools": ["tool1", "tool2"], 44 | "max_turns": 5, 45 | "temperature": 0.7 46 | } 47 | 48 | args = executor._build_args(params) 49 | 50 | assert "-p" in args 51 | assert "Test prompt" in args 52 | assert "--output-format" in args 53 | assert "json" in args 54 | assert "--system-prompt" in args 55 | assert "You are a helpful assistant" in args 56 | assert "--continue" in args 57 | assert "--resume" in args 58 | assert "session-id" in args 59 | assert "--allowedTools" in args 60 | assert "tool1,tool2" in args 61 | assert "--max-turns" in args 62 | assert "5" in args 63 | assert "--temperature" in args 64 | assert "0.7" in args 65 | 66 | @patch("subprocess.run") 67 | def test_execute_success(self, mock_run): 68 | """Test successful command execution""" 69 | # Mock the subprocess.run return value 70 | mock_process = MagicMock() 71 | mock_process.returncode = 0 72 | mock_process.stdout = '{"result": "success"}' 73 | mock_run.return_value = mock_process 74 | 75 | executor = ClaudeCliExecutor() 76 | result = executor.execute({"prompt": "Test"}) 77 | 78 | assert result == '{"result": "success"}' 79 | mock_run.assert_called_once() 80 | 81 | @patch("subprocess.run") 82 | def test_execute_error(self, mock_run): 83 | """Test command execution with error""" 84 | # Mock the subprocess.run return value 85 | mock_process = MagicMock() 86 | mock_process.returncode = 1 87 | mock_process.stderr = "Command failed" 88 | mock_run.return_value = mock_process 89 | 90 | executor = ClaudeCliExecutor() 91 | 92 | with pytest.raises(Exception) as excinfo: 93 | executor.execute({"prompt": "Test"}) 94 | 95 | assert "exited with code 1" in str(excinfo.value) 96 | assert hasattr(excinfo.value, "status") 97 | assert excinfo.value.status == 1 98 | 99 | @patch("subprocess.run") 100 | def test_execute_timeout(self, mock_run): 101 | """Test command execution with timeout""" 102 | # Mock the subprocess.run to raise TimeoutExpired 103 | mock_run.side_effect = subprocess.TimeoutExpired(cmd="test", timeout=5) 104 | 105 | executor = ClaudeCliExecutor() 106 | 107 | with pytest.raises(Exception) as excinfo: 108 | executor.execute({"prompt": "Test"}) 109 | 110 | assert "timed out" in str(excinfo.value) 111 | assert hasattr(excinfo.value, "status") 112 | assert excinfo.value.status == 408 113 | 114 | @patch("subprocess.Popen") 115 | def test_execute_stream(self, mock_popen): 116 | """Test streaming command execution""" 117 | # Mock the subprocess.Popen 118 | mock_process = MagicMock() 119 | mock_process.stdout = [ 120 | '{"type": "assistant", "content": "Hello"}', 121 | '{"type": "assistant", "content": "World"}' 122 | ] 123 | mock_process.wait.return_value = 0 124 | mock_popen.return_value = mock_process 125 | 126 | executor = ClaudeCliExecutor() 127 | stream = executor.execute_stream({"prompt": "Test"}) 128 | 129 | chunks = list(stream) 130 | assert len(chunks) == 2 131 | assert chunks[0]["type"] == "assistant" 132 | assert chunks[0]["content"] == "Hello" 133 | assert chunks[1]["type"] == "assistant" 134 | assert chunks[1]["content"] == "World" -------------------------------------------------------------------------------- /python/tests/test_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the client classes 3 | """ 4 | 5 | import pytest 6 | from unittest.mock import patch, MagicMock 7 | 8 | from claude_code_sdk import ClaudeCode 9 | from claude_code_sdk.client.base import BaseClient 10 | from claude_code_sdk.client.chat import ChatCompletions 11 | from claude_code_sdk.client.messages import Messages 12 | from claude_code_sdk.client.sessions import Sessions, Session 13 | from claude_code_sdk.client.tools import Tools 14 | 15 | 16 | class TestBaseClient: 17 | """Test suite for BaseClient""" 18 | 19 | def test_init(self): 20 | """Test initialization with default values""" 21 | client = BaseClient() 22 | assert client.default_model == "claude-code" 23 | assert client.default_timeout == 300000 24 | assert client.executor is not None 25 | 26 | def test_init_with_options(self): 27 | """Test initialization with custom options""" 28 | client = BaseClient({ 29 | "api_key": "test-key", 30 | "cli_path": "custom-path", 31 | "timeout": 60000 32 | }) 33 | assert client.api_key == "test-key" 34 | assert client.default_timeout == 60000 35 | assert client.executor.cli_path == "custom-path" 36 | 37 | def test_create_error(self): 38 | """Test error creation""" 39 | client = BaseClient() 40 | error = client.create_error("Test error", 404, "not_found") 41 | 42 | assert str(error) == "Test error" 43 | assert error.status == 404 44 | assert error.code == "not_found" 45 | 46 | @patch("claude_code_sdk.implementations.cli.ClaudeCliExecutor.execute") 47 | def test_execute_command(self, mock_execute): 48 | """Test command execution""" 49 | mock_execute.return_value = '{"result": "success"}' 50 | 51 | client = BaseClient() 52 | result = client.execute_command({"prompt": "Test"}) 53 | 54 | assert result == '{"result": "success"}' 55 | mock_execute.assert_called_once() 56 | 57 | @patch("claude_code_sdk.implementations.cli.ClaudeCliExecutor.execute") 58 | def test_execute_command_error(self, mock_execute): 59 | """Test command execution with error""" 60 | error = Exception("Command failed") 61 | setattr(error, "status", 500) 62 | mock_execute.side_effect = error 63 | 64 | client = BaseClient() 65 | 66 | with pytest.raises(Exception) as excinfo: 67 | client.execute_command({"prompt": "Test"}) 68 | 69 | assert "Command failed" in str(excinfo.value) 70 | assert hasattr(excinfo.value, "status") 71 | assert excinfo.value.status == 500 72 | 73 | 74 | class TestClaudeCode: 75 | """Test suite for ClaudeCode main client""" 76 | 77 | def test_init(self): 78 | """Test initialization""" 79 | client = ClaudeCode() 80 | 81 | assert client.chat is not None 82 | assert "completions" in client.chat 83 | assert client.messages is not None 84 | assert client.sessions is not None 85 | assert client.tools is not None 86 | 87 | def test_init_with_options(self): 88 | """Test initialization with options""" 89 | client = ClaudeCode({ 90 | "api_key": "test-key", 91 | "cli_path": "custom-path", 92 | "timeout": 60000 93 | }) 94 | 95 | assert client.api_key == "test-key" 96 | assert client.default_timeout == 60000 97 | 98 | 99 | class TestChatCompletions: 100 | """Test suite for ChatCompletions""" 101 | 102 | @patch("claude_code_sdk.client.base.BaseClient.execute_command") 103 | def test_create(self, mock_execute): 104 | """Test create completion""" 105 | mock_execute.return_value = '{"id": "test", "choices": [{"message": {"content": "Hello"}}]}' 106 | 107 | client = BaseClient() 108 | chat = ChatCompletions(client) 109 | 110 | result = chat.create({ 111 | "model": "claude-code", 112 | "messages": [{"role": "user", "content": "Hello"}] 113 | }) 114 | 115 | assert result["id"] == "test" 116 | assert result["choices"][0]["message"]["content"] == "Hello" 117 | mock_execute.assert_called_once() 118 | 119 | @patch("claude_code_sdk.client.base.BaseClient.execute_stream_command") 120 | def test_create_stream(self, mock_stream): 121 | """Test create streaming completion""" 122 | mock_stream.return_value = [ 123 | {"choices": [{"delta": {"content": "Hello"}}]}, 124 | {"choices": [{"delta": {"content": " world"}}]} 125 | ] 126 | 127 | client = BaseClient() 128 | chat = ChatCompletions(client) 129 | 130 | stream = chat.create_stream({ 131 | "model": "claude-code", 132 | "messages": [{"role": "user", "content": "Hello"}], 133 | "stream": True 134 | }) 135 | 136 | assert mock_stream.called 137 | 138 | @patch("claude_code_sdk.client.chat.ChatCompletions.create") 139 | def test_create_with_tools(self, mock_create): 140 | """Test create with tools""" 141 | mock_create.return_value = {"id": "test"} 142 | 143 | client = BaseClient() 144 | chat = ChatCompletions(client) 145 | 146 | result = chat.create({ 147 | "model": "claude-code", 148 | "messages": [{"role": "user", "content": "Hello"}], 149 | "tools": [ 150 | { 151 | "name": "calculator", 152 | "description": "Calculate expressions", 153 | "parameters": {"type": "object"} 154 | } 155 | ] 156 | }) 157 | 158 | assert result["id"] == "test" 159 | mock_create.assert_called_once() 160 | 161 | 162 | class TestMessages: 163 | """Test suite for Messages""" 164 | 165 | @patch("claude_code_sdk.client.base.BaseClient.execute_command") 166 | def test_create(self, mock_execute): 167 | """Test create message""" 168 | mock_execute.return_value = '{"id": "test", "choices": [{"message": {"content": "Hello"}}]}' 169 | 170 | client = BaseClient() 171 | messages = Messages(client) 172 | 173 | result = messages.create({ 174 | "model": "claude-code", 175 | "messages": [ 176 | { 177 | "role": "user", 178 | "content": [{"type": "text", "text": "Hello"}] 179 | } 180 | ] 181 | }) 182 | 183 | assert "choices" in result 184 | mock_execute.assert_called_once() 185 | 186 | @patch("claude_code_sdk.client.base.BaseClient.execute_stream_command") 187 | def test_create_stream(self, mock_stream): 188 | """Test create streaming message""" 189 | mock_stream.return_value = [ 190 | {"type": "content_block_delta", "delta": {"text": "Hello"}}, 191 | {"type": "content_block_delta", "delta": {"text": " world"}} 192 | ] 193 | 194 | client = BaseClient() 195 | messages = Messages(client) 196 | 197 | stream = messages.create_stream({ 198 | "model": "claude-code", 199 | "messages": [ 200 | { 201 | "role": "user", 202 | "content": [{"type": "text", "text": "Hello"}] 203 | } 204 | ], 205 | "stream": True 206 | }) 207 | 208 | assert mock_stream.called 209 | 210 | 211 | class TestSessions: 212 | """Test suite for Sessions""" 213 | 214 | @patch("claude_code_sdk.client.chat.ChatCompletions.create") 215 | def test_create(self, mock_create): 216 | """Test create session""" 217 | mock_create.return_value = {"id": "test", "session_id": "session123"} 218 | 219 | client = BaseClient() 220 | sessions = Sessions(client) 221 | 222 | session = sessions.create({ 223 | "messages": [{"role": "user", "content": "Hello"}] 224 | }) 225 | 226 | assert isinstance(session, Session) 227 | assert session.id == "session123" 228 | mock_create.assert_called_once() 229 | 230 | def test_resume(self): 231 | """Test resume session""" 232 | client = BaseClient() 233 | sessions = Sessions(client) 234 | 235 | session = sessions.resume("session123") 236 | 237 | assert isinstance(session, Session) 238 | assert session.id == "session123" 239 | 240 | 241 | class TestSession: 242 | """Test suite for Session""" 243 | 244 | @patch("claude_code_sdk.client.chat.ChatCompletions.create") 245 | def test_continue_session(self, mock_create): 246 | """Test continue session""" 247 | mock_create.return_value = {"id": "test", "choices": [{"message": {"content": "Hello"}}]} 248 | 249 | client = BaseClient() 250 | session = Session(client, "session123") 251 | 252 | result = session.continue_session({ 253 | "messages": [{"role": "user", "content": "Hello"}] 254 | }) 255 | 256 | assert result["id"] == "test" 257 | mock_create.assert_called_once() 258 | 259 | # Check that resume parameter was added 260 | args = mock_create.call_args[0][0] 261 | assert args["resume"] == "session123" 262 | 263 | @patch("claude_code_sdk.client.chat.ChatCompletions.create_stream") 264 | def test_continue_stream(self, mock_stream): 265 | """Test continue session with streaming""" 266 | mock_stream.return_value = [ 267 | {"choices": [{"delta": {"content": "Hello"}}]}, 268 | {"choices": [{"delta": {"content": " world"}}]} 269 | ] 270 | 271 | client = BaseClient() 272 | session = Session(client, "session123") 273 | 274 | stream = session.continue_stream({ 275 | "messages": [{"role": "user", "content": "Hello"}] 276 | }) 277 | 278 | assert mock_stream.called 279 | 280 | # Check that resume parameter was added 281 | args = mock_stream.call_args[0][0] 282 | assert args["resume"] == "session123" 283 | assert args["stream"] == True 284 | 285 | 286 | class TestTools: 287 | """Test suite for Tools""" 288 | 289 | def test_create(self): 290 | """Test create tool""" 291 | client = BaseClient() 292 | tools = Tools(client) 293 | 294 | tool = tools.create({ 295 | "name": "calculator", 296 | "description": "Perform calculations", 297 | "input_schema": { 298 | "type": "object", 299 | "properties": { 300 | "expression": {"type": "string"} 301 | } 302 | } 303 | }) 304 | 305 | assert tool["name"] == "calculator" 306 | assert tool["description"] == "Perform calculations" 307 | 308 | def test_get(self): 309 | """Test get tool""" 310 | client = BaseClient() 311 | tools = Tools(client) 312 | 313 | tools.create({ 314 | "name": "calculator", 315 | "description": "Perform calculations" 316 | }) 317 | 318 | tool = tools.get("calculator") 319 | 320 | assert tool is not None 321 | assert tool["name"] == "calculator" 322 | 323 | # Test non-existent tool 324 | tool = tools.get("non-existent") 325 | assert tool is None 326 | 327 | def test_list(self): 328 | """Test list tools""" 329 | client = BaseClient() 330 | tools = Tools(client) 331 | 332 | tools.create({"name": "tool1"}) 333 | tools.create({"name": "tool2"}) 334 | 335 | tool_list = tools.list() 336 | 337 | assert len(tool_list) == 2 338 | assert tool_list[0]["name"] == "tool1" 339 | assert tool_list[1]["name"] == "tool2" 340 | 341 | def test_delete(self): 342 | """Test delete tool""" 343 | client = BaseClient() 344 | tools = Tools(client) 345 | 346 | tools.create({"name": "tool1"}) 347 | 348 | # Delete existing tool 349 | result = tools.delete("tool1") 350 | assert result is True 351 | assert tools.get("tool1") is None 352 | 353 | # Delete non-existent tool 354 | result = tools.delete("non-existent") 355 | assert result is False -------------------------------------------------------------------------------- /python/tests/test_converters.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the converters 3 | """ 4 | 5 | import pytest 6 | import json 7 | from claude_code_sdk.implementations.converters import ( 8 | convert_messages_to_prompt, 9 | convert_anthropic_messages_to_prompt, 10 | parse_cli_output, 11 | convert_openai_to_anthropic_tools, 12 | convert_anthropic_to_openai_response 13 | ) 14 | 15 | 16 | class TestConverters: 17 | """Test suite for converter functions""" 18 | 19 | def test_convert_messages_to_prompt(self): 20 | """Test converting OpenAI-style messages to prompt""" 21 | messages = [ 22 | {"role": "system", "content": "You are a helpful assistant"}, 23 | {"role": "user", "content": "Hello"}, 24 | {"role": "assistant", "content": "Hi there"}, 25 | {"role": "user", "content": "How are you?"} 26 | ] 27 | 28 | prompt = convert_messages_to_prompt(messages) 29 | 30 | # System messages should be skipped 31 | assert "You are a helpful assistant" not in prompt 32 | 33 | # User and assistant messages should be included 34 | assert "User: Hello" in prompt 35 | assert "Assistant: Hi there" in prompt 36 | assert "User: How are you?" in prompt 37 | 38 | def test_convert_anthropic_messages_to_prompt(self): 39 | """Test converting Anthropic-style messages to prompt""" 40 | # Test with string content 41 | messages = [ 42 | {"role": "system", "content": "You are a helpful assistant"}, 43 | {"role": "user", "content": "Hello"}, 44 | {"role": "assistant", "content": "Hi there"} 45 | ] 46 | 47 | prompt = convert_anthropic_messages_to_prompt(messages) 48 | 49 | # System messages should be skipped 50 | assert "You are a helpful assistant" not in prompt 51 | 52 | # User and assistant messages should be included 53 | assert "User: Hello" in prompt 54 | assert "Assistant: Hi there" in prompt 55 | 56 | # Test with content blocks 57 | messages = [ 58 | {"role": "user", "content": [ 59 | {"type": "text", "text": "Hello"} 60 | ]}, 61 | {"role": "assistant", "content": [ 62 | {"type": "text", "text": "Hi there"} 63 | ]} 64 | ] 65 | 66 | prompt = convert_anthropic_messages_to_prompt(messages) 67 | 68 | assert "User: Hello" in prompt 69 | assert "Assistant: Hi there" in prompt 70 | 71 | def test_parse_cli_output(self): 72 | """Test parsing CLI output""" 73 | # Test with valid JSON 74 | output = '{"id": "test", "choices": [{"message": {"content": "Hello"}}]}' 75 | result = parse_cli_output(output) 76 | 77 | assert result["id"] == "test" 78 | assert result["choices"][0]["message"]["content"] == "Hello" 79 | 80 | # Test with non-JSON output 81 | output = "Plain text response" 82 | result = parse_cli_output(output) 83 | 84 | assert "choices" in result 85 | assert result["choices"][0]["message"]["role"] == "assistant" 86 | assert result["choices"][0]["message"]["content"] == "Plain text response" 87 | 88 | def test_convert_openai_to_anthropic_tools(self): 89 | """Test converting OpenAI tools to Anthropic format""" 90 | openai_tools = [ 91 | { 92 | "name": "calculator", 93 | "description": "Perform calculations", 94 | "parameters": { 95 | "type": "object", 96 | "properties": { 97 | "expression": {"type": "string"} 98 | } 99 | } 100 | } 101 | ] 102 | 103 | anthropic_tools = convert_openai_to_anthropic_tools(openai_tools) 104 | 105 | assert len(anthropic_tools) == 1 106 | assert anthropic_tools[0]["name"] == "calculator" 107 | assert anthropic_tools[0]["description"] == "Perform calculations" 108 | assert "input_schema" in anthropic_tools[0] 109 | assert anthropic_tools[0]["input_schema"]["properties"]["expression"]["type"] == "string" 110 | 111 | def test_convert_anthropic_to_openai_response(self): 112 | """Test converting Anthropic response to OpenAI format""" 113 | # Test with already OpenAI format 114 | openai_response = { 115 | "id": "test", 116 | "choices": [ 117 | { 118 | "message": { 119 | "role": "assistant", 120 | "content": "Hello" 121 | } 122 | } 123 | ] 124 | } 125 | 126 | result = convert_anthropic_to_openai_response(openai_response) 127 | assert result == openai_response 128 | 129 | # Test with Anthropic format 130 | anthropic_response = { 131 | "id": "test", 132 | "content": [ 133 | {"type": "text", "text": "Hello world"} 134 | ], 135 | "session_id": "session123" 136 | } 137 | 138 | result = convert_anthropic_to_openai_response(anthropic_response) 139 | 140 | assert result["id"] == "test" 141 | assert result["session_id"] == "session123" 142 | assert result["choices"][0]["message"]["role"] == "assistant" 143 | assert result["choices"][0]["message"]["content"] == "Hello world" -------------------------------------------------------------------------------- /python/tests/test_integration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Integration tests for Claude Code SDK 3 | """ 4 | 5 | import os 6 | import pytest 7 | from unittest.mock import patch, MagicMock 8 | 9 | from claude_code_sdk import ClaudeCode 10 | 11 | 12 | # Skip these tests if no API key is available 13 | pytestmark = pytest.mark.skipif( 14 | not os.environ.get("ANTHROPIC_API_KEY"), 15 | reason="ANTHROPIC_API_KEY environment variable not set" 16 | ) 17 | 18 | 19 | class TestIntegration: 20 | """Integration tests for Claude Code SDK""" 21 | 22 | @patch("claude_code_sdk.implementations.cli.ClaudeCliExecutor.execute") 23 | def test_openai_style_completion(self, mock_execute): 24 | """Test OpenAI-style completion""" 25 | # Mock the CLI response 26 | mock_execute.return_value = ''' 27 | { 28 | "id": "test-id", 29 | "choices": [ 30 | { 31 | "message": { 32 | "role": "assistant", 33 | "content": "Hello, I'm Claude!" 34 | } 35 | } 36 | ], 37 | "session_id": "session123" 38 | } 39 | ''' 40 | 41 | claude = ClaudeCode() 42 | response = claude.chat["completions"].create({ 43 | "model": "claude-code", 44 | "messages": [ 45 | {"role": "user", "content": "Hello, who are you?"} 46 | ] 47 | }) 48 | 49 | assert response["id"] == "test-id" 50 | assert response["choices"][0]["message"]["role"] == "assistant" 51 | assert response["choices"][0]["message"]["content"] == "Hello, I'm Claude!" 52 | assert response["session_id"] == "session123" 53 | 54 | @patch("claude_code_sdk.implementations.cli.ClaudeCliExecutor.execute") 55 | def test_anthropic_style_completion(self, mock_execute): 56 | """Test Anthropic-style completion""" 57 | # Mock the CLI response 58 | mock_execute.return_value = ''' 59 | { 60 | "id": "test-id", 61 | "content": [ 62 | { 63 | "type": "text", 64 | "text": "Hello, I'm Claude!" 65 | } 66 | ], 67 | "session_id": "session123" 68 | } 69 | ''' 70 | 71 | claude = ClaudeCode() 72 | response = claude.messages.create({ 73 | "model": "claude-code", 74 | "messages": [ 75 | { 76 | "role": "user", 77 | "content": [ 78 | {"type": "text", "text": "Hello, who are you?"} 79 | ] 80 | } 81 | ] 82 | }) 83 | 84 | assert "choices" in response 85 | assert response["choices"][0]["message"]["content"] == "Hello, I'm Claude!" 86 | 87 | @patch("claude_code_sdk.implementations.cli.ClaudeCliExecutor.execute") 88 | def test_session_management(self, mock_execute): 89 | """Test session management""" 90 | # Mock the CLI response for session creation 91 | mock_execute.return_value = ''' 92 | { 93 | "id": "test-id", 94 | "choices": [ 95 | { 96 | "message": { 97 | "role": "assistant", 98 | "content": "Hello, I'm Claude!" 99 | } 100 | } 101 | ], 102 | "session_id": "session123" 103 | } 104 | ''' 105 | 106 | claude = ClaudeCode() 107 | session = claude.sessions.create({ 108 | "messages": [ 109 | {"role": "user", "content": "Hello, who are you?"} 110 | ] 111 | }) 112 | 113 | assert session.id == "session123" 114 | 115 | # Mock the CLI response for session continuation 116 | mock_execute.return_value = ''' 117 | { 118 | "id": "test-id-2", 119 | "choices": [ 120 | { 121 | "message": { 122 | "role": "assistant", 123 | "content": "I'm doing well, thank you!" 124 | } 125 | } 126 | ], 127 | "session_id": "session123" 128 | } 129 | ''' 130 | 131 | response = session.continue_session({ 132 | "messages": [ 133 | {"role": "user", "content": "How are you?"} 134 | ] 135 | }) 136 | 137 | assert response["id"] == "test-id-2" 138 | assert response["choices"][0]["message"]["content"] == "I'm doing well, thank you!" 139 | 140 | def test_tool_management(self): 141 | """Test tool management""" 142 | claude = ClaudeCode() 143 | 144 | # Create a tool 145 | tool = claude.tools.create({ 146 | "name": "calculator", 147 | "description": "Perform calculations", 148 | "input_schema": { 149 | "type": "object", 150 | "properties": { 151 | "expression": {"type": "string"} 152 | }, 153 | "required": ["expression"] 154 | } 155 | }) 156 | 157 | assert tool["name"] == "calculator" 158 | 159 | # Get the tool 160 | retrieved_tool = claude.tools.get("calculator") 161 | assert retrieved_tool["name"] == "calculator" 162 | assert retrieved_tool["description"] == "Perform calculations" 163 | 164 | # List tools 165 | tools = claude.tools.list() 166 | assert len(tools) == 1 167 | assert tools[0]["name"] == "calculator" 168 | 169 | # Delete the tool 170 | result = claude.tools.delete("calculator") 171 | assert result is True 172 | 173 | # Verify it's gone 174 | assert claude.tools.get("calculator") is None -------------------------------------------------------------------------------- /typescript/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended" 8 | ], 9 | "rules": { 10 | "@typescript-eslint/explicit-function-return-type": "error", 11 | "@typescript-eslint/no-explicit-any": "error" 12 | }, 13 | "env": { 14 | "node": true, 15 | "es2020": true 16 | } 17 | } -------------------------------------------------------------------------------- /typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js dependencies 2 | node_modules/ 3 | 4 | # TypeScript build output 5 | dist/ 6 | 7 | # IDE files 8 | .vscode/ 9 | .idea/ 10 | 11 | # Environment variables 12 | .env 13 | .env.local 14 | 15 | # Logs 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # OS-specific files 22 | .DS_Store 23 | Thumbs.db 24 | 25 | # Test coverage 26 | coverage/ -------------------------------------------------------------------------------- /typescript/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2 7 | } -------------------------------------------------------------------------------- /typescript/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Claude Code SDK (TypeScript) 2 | 3 | Thank you for your interest in contributing to the Claude Code SDK! This document provides guidelines and instructions for contributing to the TypeScript implementation. 4 | 5 | ## Code of Conduct 6 | 7 | Please be respectful and considerate of others when contributing to this project. We aim to foster an inclusive and welcoming community. 8 | 9 | ## Getting Started 10 | 11 | 1. **Fork the repository** and clone it locally 12 | 2. **Install dependencies**: 13 | ```bash 14 | npm install 15 | ``` 16 | 3. **Build the project**: 17 | ```bash 18 | npm run build 19 | ``` 20 | 4. **Run tests**: 21 | ```bash 22 | npm run test 23 | ``` 24 | 25 | ## Development Workflow 26 | 27 | 1. **Create a branch** for your feature or bugfix: 28 | ```bash 29 | git checkout -b feature/your-feature-name 30 | ``` 31 | 32 | 2. **Make your changes** following the code style guidelines 33 | 34 | 3. **Write tests** for your changes 35 | 36 | 4. **Run linting and type checking**: 37 | ```bash 38 | npm run lint 39 | npm run typecheck 40 | ``` 41 | 42 | 5. **Format your code**: 43 | ```bash 44 | npm run format:fix 45 | ``` 46 | 47 | 6. **Commit your changes** with a descriptive commit message 48 | 49 | 7. **Push your branch** and submit a pull request 50 | 51 | ## Code Style Guidelines 52 | 53 | - Follow the existing code style (no semicolons, single quotes, 2-space indentation) 54 | - Use explicit function return types 55 | - Avoid using `any` type 56 | - Use camelCase for variables/functions and PascalCase for classes/interfaces 57 | - Write comprehensive JSDoc comments for public APIs 58 | - Use named exports rather than default exports 59 | - Follow ESM module patterns 60 | 61 | ## Testing 62 | 63 | - Write unit tests for all new functionality 64 | - Ensure all tests pass before submitting a pull request 65 | - Mock external dependencies in tests 66 | 67 | ## Documentation 68 | 69 | - Update documentation for any changed functionality 70 | - Document all public APIs with JSDoc comments 71 | - Include examples for new features 72 | 73 | ## Pull Request Process 74 | 75 | 1. Ensure your code follows the style guidelines 76 | 2. Update the README.md with details of changes if appropriate 77 | 3. The PR should work on the main branch 78 | 4. Include a description of the changes in your PR 79 | 80 | ## Release Process 81 | 82 | Releases are managed by the maintainers. The general process is: 83 | 84 | 1. Update version in package.json 85 | 2. Update CHANGELOG.md 86 | 3. Create a new release on GitHub 87 | 88 | ## Getting Help 89 | 90 | If you have questions or need help, please open an issue on GitHub. 91 | 92 | Thank you for contributing to the Claude Code SDK! -------------------------------------------------------------------------------- /typescript/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /typescript/README.md: -------------------------------------------------------------------------------- 1 | # Claude Code SDK 2 | 3 | A TypeScript wrapper for Claude Code CLI that provides a seamless, type-safe API compatible with both OpenAI and Anthropic SDKs. 4 | 5 | ## Installation 6 | 7 | First, install the Claude Code CLI: 8 | 9 | ```bash 10 | npm install -g @anthropic-ai/claude-code 11 | ``` 12 | 13 | Then install the wrapper using one of these methods: 14 | 15 | ```bash 16 | # Using npm 17 | npm install claude-code-sdk 18 | 19 | # Using yarn 20 | yarn add claude-code-sdk 21 | 22 | # Using pnpm 23 | pnpm add claude-code-sdk 24 | ``` 25 | 26 | ## Setup 27 | 28 | You'll need an Anthropic API key to use Claude Code. You can either set it as an environment variable: 29 | 30 | ```bash 31 | export ANTHROPIC_API_KEY=your_api_key_here 32 | ``` 33 | 34 | Or provide it when initializing the client: 35 | 36 | ```typescript 37 | import { ClaudeCode } from 'claude-code-sdk' 38 | 39 | const claude = new ClaudeCode({ 40 | apiKey: 'your_api_key_here' 41 | }) 42 | ``` 43 | 44 | ## Usage 45 | 46 | This SDK provides both OpenAI-style and Anthropic-style APIs for interacting with Claude Code. 47 | 48 | ### OpenAI Style API 49 | 50 | ```typescript 51 | import { ClaudeCode } from 'claude-code-sdk' 52 | 53 | // Create a client 54 | const claude = new ClaudeCode() 55 | 56 | // Use OpenAI-style completions API 57 | async function generateCode() { 58 | const response = await claude.chat.completions.create({ 59 | model: 'claude-code', 60 | messages: [ 61 | { role: 'user', content: 'Write a TypeScript function to read CSV files' } 62 | ], 63 | max_tokens: 1000, 64 | temperature: 0.7, 65 | }) 66 | 67 | console.log(response.choices[0].message.content) 68 | } 69 | 70 | // Streaming example 71 | async function streamCode() { 72 | const stream = await claude.chat.completions.createStream({ 73 | model: 'claude-code', 74 | messages: [ 75 | { role: 'user', content: 'Create a React component for a login form' } 76 | ] 77 | }) 78 | 79 | for await (const chunk of stream) { 80 | if (chunk.choices[0].delta.content) { 81 | process.stdout.write(chunk.choices[0].delta.content) 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | ### Anthropic Style API 88 | 89 | ```typescript 90 | import { ClaudeCode } from 'claude-code-sdk' 91 | 92 | // Create a client 93 | const claude = new ClaudeCode() 94 | 95 | // Use Anthropic-style messages API 96 | async function generateCode() { 97 | const response = await claude.messages.create({ 98 | model: 'claude-code', 99 | messages: [ 100 | { 101 | role: 'user', 102 | content: [{ 103 | type: 'text', 104 | text: 'Write a TypeScript function to read CSV files' 105 | }] 106 | } 107 | ], 108 | max_tokens: 1000, 109 | }) 110 | 111 | console.log(response.content[0].text) 112 | } 113 | 114 | // Streaming example 115 | async function streamCode() { 116 | const stream = await claude.messages.createStream({ 117 | model: 'claude-code', 118 | messages: [ 119 | { 120 | role: 'user', 121 | content: [{ 122 | type: 'text', 123 | text: 'Create a React component for a login form' 124 | }] 125 | } 126 | ] 127 | }) 128 | 129 | for await (const chunk of stream) { 130 | if (chunk.type === 'content_block_delta' && chunk.delta?.text) { 131 | process.stdout.write(chunk.delta.text) 132 | } 133 | } 134 | } 135 | ``` 136 | 137 | ### Session Management 138 | 139 | ```typescript 140 | import { ClaudeCode } from 'claude-code-sdk' 141 | 142 | const claude = new ClaudeCode() 143 | 144 | async function codeSession() { 145 | // Start a session 146 | const session = await claude.sessions.create({ 147 | messages: [ 148 | { role: 'user', content: 'Let\'s create a TypeScript project' } 149 | ] 150 | }) 151 | 152 | // Continue the session 153 | const response = await session.continue({ 154 | messages: [ 155 | { role: 'user', content: 'Now add a database connection' } 156 | ] 157 | }) 158 | 159 | console.log(response.choices[0].message.content) 160 | } 161 | ``` 162 | 163 | ### Tools 164 | 165 | ```typescript 166 | import { ClaudeCode } from 'claude-code-sdk' 167 | 168 | const claude = new ClaudeCode() 169 | 170 | async function useTools() { 171 | // Register a tool 172 | await claude.tools.create({ 173 | name: 'filesystem', 174 | description: 'Access the filesystem', 175 | input_schema: { 176 | type: 'object', 177 | properties: { 178 | path: { type: 'string' } 179 | }, 180 | required: ['path'] 181 | } 182 | }) 183 | 184 | // Use the tool in a chat completion 185 | const response = await claude.chat.completions.create({ 186 | model: 'claude-code', 187 | messages: [ 188 | { role: 'user', content: 'Read my README.md file' } 189 | ], 190 | tools: [{ name: 'filesystem' }] 191 | }) 192 | 193 | console.log(response.choices[0].message.content) 194 | } 195 | ``` 196 | 197 | ## Debugging 198 | 199 | To test if the Claude Code CLI is installed and configured correctly, run: 200 | 201 | ```bash 202 | npx claude -h 203 | ``` 204 | 205 | If you experience issues, set more verbose output: 206 | 207 | ```typescript 208 | const claude = new ClaudeCode({ 209 | apiKey: process.env.ANTHROPIC_API_KEY, 210 | cliPath: '/path/to/claude', // If claude isn't in your PATH 211 | timeout: 60000 // Longer timeout (ms) 212 | }) 213 | ``` 214 | 215 | ## Features 216 | 217 | - OpenAI-compatible `chat.completions.create` method 218 | - Anthropic-compatible `messages.create` method 219 | - Session management for multi-turn conversations 220 | - Tool registration and usage 221 | - Full TypeScript support 222 | - Streaming responses 223 | - Batch operations 224 | 225 | ## Requirements 226 | 227 | - Node.js v16+ 228 | - TypeScript 4.5+ 229 | - @anthropic-ai/claude-code CLI installed 230 | 231 | ## License 232 | 233 | MIT -------------------------------------------------------------------------------- /typescript/TEST-REPORT.md: -------------------------------------------------------------------------------- 1 | # Claude Code SDK Test Report 2 | 3 | ## Overview 4 | The Claude Code SDK TypeScript wrapper has been thoroughly tested with 33 unit tests across 4 test files. All tests are currently passing. 5 | 6 | ## Test Coverage 7 | 8 | | Component | Tests | Status | 9 | |-----------|-------|--------| 10 | | Client | 15 | ✅ PASS | 11 | | Sessions | 6 | ✅ PASS | 12 | | Converters| 11 | ✅ PASS | 13 | | CLI | 1 | ✅ PASS | 14 | | **Total** | **33**| ✅ PASS | 15 | 16 | ## Test Details 17 | 18 | ### Client Tests 19 | - Client initialization tests 20 | - OpenAI-style API tests (chat.completions) 21 | - Anthropic-style API tests (messages) 22 | - Session management tests 23 | - Tools API tests 24 | 25 | ### Sessions Tests 26 | - Session creation tests 27 | - Session resumption tests 28 | - Message continuation tests 29 | - Message retrieval tests 30 | 31 | ### Converters Tests 32 | - OpenAI to Anthropic message conversion tests 33 | - Anthropic to OpenAI message conversion tests 34 | - OpenAI to Anthropic tools conversion tests 35 | - Anthropic to OpenAI tools conversion tests 36 | - Messages to prompt format conversion tests 37 | - CLI output parsing tests 38 | 39 | ### CLI Tests 40 | - Command building tests with various parameters 41 | 42 | ## Future Test Improvements 43 | 44 | 1. **Integration Tests**: Add integration tests that actually invoke the Claude Code CLI (with appropriate mocking) 45 | 2. **Stream Tests**: Add more robust tests for streaming responses 46 | 3. **Error Handling**: Add more tests for error conditions and edge cases 47 | 4. **Type Tests**: Add type tests to ensure the TypeScript types are working correctly 48 | 5. **Coverage Analysis**: Add test coverage metrics and ensure high coverage across all components 49 | 50 | ## Running the Tests 51 | 52 | Tests can be run using the following command: 53 | 54 | ```bash 55 | npm test 56 | ``` 57 | 58 | This executes all the tests using Vitest and produces a consolidated report. -------------------------------------------------------------------------------- /typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "claude-code-sdk", 3 | "version": "0.1.0", 4 | "description": "TypeScript wrapper for Claude Code SDK", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "build:dev": "tsc --watch", 10 | "test": "vitest run", 11 | "lint": "eslint src --ext .ts", 12 | "format": "prettier --check \"src/**/*.ts\"", 13 | "format:fix": "prettier --write \"src/**/*.ts\"", 14 | "typecheck": "tsc --noEmit", 15 | "prepare-package": "node ./scripts/prepare-package.js", 16 | "prepublishOnly": "npm run build" 17 | }, 18 | "keywords": [ 19 | "claude", 20 | "anthropic", 21 | "ai", 22 | "code", 23 | "sdk" 24 | ], 25 | "author": "", 26 | "license": "MIT", 27 | "type": "module", 28 | "devDependencies": { 29 | "@types/node": "^22.15.19", 30 | "@typescript-eslint/eslint-plugin": "^8.32.1", 31 | "@typescript-eslint/parser": "^8.32.1", 32 | "eslint": "^9.27.0", 33 | "prettier": "^3.5.3", 34 | "typescript": "^5.8.3", 35 | "vitest": "^3.1.4" 36 | }, 37 | "peerDependencies": { 38 | "@anthropic-ai/claude-code": "*" 39 | }, 40 | "engines": { 41 | "node": ">=16.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /typescript/scripts/prepare-package.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script prepares the package for publishing by: 5 | * 1. Cleaning the dist directory 6 | * 2. Running the build 7 | * 3. Copying additional files to the dist directory 8 | */ 9 | 10 | import fs from 'fs'; 11 | import path from 'path'; 12 | import { execSync } from 'child_process'; 13 | import { fileURLToPath } from 'url'; 14 | 15 | // Get __dirname equivalent in ESM 16 | const __filename = fileURLToPath(import.meta.url); 17 | const __dirname = path.dirname(__filename); 18 | 19 | // Paths 20 | const rootDir = path.resolve(__dirname, '..'); 21 | const distDir = path.join(rootDir, 'dist'); 22 | 23 | // Make sure dist directory exists 24 | if (!fs.existsSync(distDir)) { 25 | fs.mkdirSync(distDir, { recursive: true }); 26 | } 27 | 28 | console.log('Building package...'); 29 | try { 30 | execSync('npm run build', { stdio: 'inherit', cwd: rootDir }); 31 | } catch (error) { 32 | console.error('Build failed:', error); 33 | process.exit(1); 34 | } 35 | 36 | // Files to copy to dist 37 | const filesToCopy = [ 38 | 'README.md', 39 | 'LICENSE', 40 | 'package.json' 41 | ]; 42 | 43 | console.log('Copying package files to dist...'); 44 | filesToCopy.forEach(file => { 45 | const srcPath = path.join(rootDir, file); 46 | const destPath = path.join(distDir, file); 47 | 48 | if (fs.existsSync(srcPath)) { 49 | fs.copyFileSync(srcPath, destPath); 50 | console.log(`Copied ${file} to dist`); 51 | } else { 52 | console.warn(`Warning: ${file} not found`); 53 | } 54 | }); 55 | 56 | // Create a LICENSE file if it doesn't exist 57 | if (!fs.existsSync(path.join(rootDir, 'LICENSE'))) { 58 | console.log('Creating LICENSE file...'); 59 | const licenseContent = `MIT License 60 | 61 | Copyright (c) ${new Date().getFullYear()} 62 | 63 | Permission is hereby granted, free of charge, to any person obtaining a copy 64 | of this software and associated documentation files (the "Software"), to deal 65 | in the Software without restriction, including without limitation the rights 66 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 67 | copies of the Software, and to permit persons to whom the Software is 68 | furnished to do so, subject to the following conditions: 69 | 70 | The above copyright notice and this permission notice shall be included in all 71 | copies or substantial portions of the Software. 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 74 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 75 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 76 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 77 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 78 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 79 | SOFTWARE.`; 80 | 81 | fs.writeFileSync(path.join(rootDir, 'LICENSE'), licenseContent); 82 | fs.copyFileSync(path.join(rootDir, 'LICENSE'), path.join(distDir, 'LICENSE')); 83 | console.log('Created and copied LICENSE file'); 84 | } 85 | 86 | // Create a package.json for the dist directory 87 | const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8')); 88 | 89 | // Remove development-only properties 90 | delete packageJson.devDependencies; 91 | delete packageJson.scripts; 92 | 93 | // Update paths 94 | packageJson.main = 'index.js'; 95 | packageJson.types = 'index.d.ts'; 96 | 97 | // Write the modified package.json to dist 98 | fs.writeFileSync( 99 | path.join(distDir, 'package.json'), 100 | JSON.stringify(packageJson, null, 2) 101 | ); 102 | 103 | console.log('Package is ready for publishing!'); 104 | console.log('To publish, run: cd dist && npm publish'); -------------------------------------------------------------------------------- /typescript/scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to build and publish the package 4 | 5 | # Ensure we're running from the correct directory 6 | cd "$(dirname "$0")/.." || exit 1 7 | 8 | # Check if NPM_TOKEN is set 9 | if [ -z "$NPM_TOKEN" ]; then 10 | echo "Error: NPM_TOKEN environment variable is not set" 11 | exit 1 12 | fi 13 | 14 | # Clean the dist directory 15 | rm -rf dist 16 | 17 | # Run tests 18 | npm test 19 | 20 | # Run linting and type checking 21 | npm run lint 22 | npm run typecheck 23 | 24 | # Build the package 25 | npm run build 26 | 27 | # Create .npmrc file with auth token 28 | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc 29 | 30 | # Publish the package 31 | npm publish --access public 32 | 33 | # Remove .npmrc 34 | rm .npmrc 35 | 36 | echo "Package published successfully!" -------------------------------------------------------------------------------- /typescript/scripts/test-real-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // This script tests the actual CLI interaction to verify it works correctly 4 | 5 | const { spawn } = require('child_process'); 6 | const { exec } = require('child_process'); 7 | 8 | // Check if the claude CLI is installed 9 | exec('which claude || echo "not-found"', (error, stdout, stderr) => { 10 | if (error) { 11 | console.error('Error checking for Claude CLI:', error); 12 | return; 13 | } 14 | 15 | if (stdout.trim() === 'not-found') { 16 | console.error('\x1b[31mERROR: Claude CLI not found in PATH\x1b[0m'); 17 | console.log('Please install the Claude CLI first:'); 18 | console.log('npm install -g @anthropic-ai/claude-code'); 19 | return; 20 | } 21 | 22 | console.log(`\x1b[32mFound Claude CLI at: ${stdout.trim()}\x1b[0m`); 23 | 24 | // Test a simple command to verify CLI works 25 | console.log('\nTesting basic CLI functionality...'); 26 | const claudeProcess = spawn('claude', ['-h'], { stdio: 'pipe' }); 27 | 28 | let output = ''; 29 | 30 | claudeProcess.stdout.on('data', (data) => { 31 | output += data.toString(); 32 | }); 33 | 34 | claudeProcess.stderr.on('data', (data) => { 35 | console.error(`\x1b[31mCLI Error: ${data.toString()}\x1b[0m`); 36 | }); 37 | 38 | claudeProcess.on('close', (code) => { 39 | if (code === 0) { 40 | console.log('\x1b[32mBasic CLI functionality works!\x1b[0m'); 41 | console.log('Sample CLI help output:'); 42 | console.log('-'.repeat(50)); 43 | console.log(output.split('\n').slice(0, 10).join('\n') + '\n...'); 44 | console.log('-'.repeat(50)); 45 | console.log('\nYour Claude Code SDK wrapper should be able to interact with the CLI.'); 46 | console.log('\nNext steps:'); 47 | console.log('1. Build the TypeScript wrapper: npm run build'); 48 | console.log('2. Run a test example: node dist/examples/basic.js'); 49 | } else { 50 | console.error(`\x1b[31mCLI test failed with code ${code}\x1b[0m`); 51 | console.log('Please ensure the Claude CLI is correctly installed and working.'); 52 | } 53 | }); 54 | }); -------------------------------------------------------------------------------- /typescript/src/client/base.ts: -------------------------------------------------------------------------------- 1 | import { ClaudeCliExecutor, ClaudeExecParams } from '../implementations/cli' 2 | import { ClaudeCodeOptions, ClaudeCodeError } from '../types' 3 | 4 | export class BaseClient { 5 | protected executor: ClaudeCliExecutor 6 | protected apiKey?: string 7 | protected defaultModel: string = 'claude-code' 8 | protected defaultTimeout: number 9 | 10 | constructor(options: ClaudeCodeOptions = {}) { 11 | this.apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY 12 | this.defaultTimeout = options.timeout || 300000 // 5 minutes default 13 | 14 | this.executor = new ClaudeCliExecutor({ 15 | cliPath: options.cliPath || '@anthropic-ai/claude-code', 16 | timeout: this.defaultTimeout, 17 | env: { 18 | ...(this.apiKey ? { ANTHROPIC_API_KEY: this.apiKey } : {}), 19 | }, 20 | }) 21 | } 22 | 23 | /** 24 | * Creates an error object in the style of OpenAI/Anthropic SDKs 25 | */ 26 | protected createError(message: string, status: number = 500, code?: string): ClaudeCodeError { 27 | const error = new Error(message) as ClaudeCodeError 28 | error.status = status 29 | error.code = code 30 | return error 31 | } 32 | 33 | /** 34 | * Executes a Claude CLI command with error handling 35 | */ 36 | protected async executeCommand(params: ClaudeExecParams): Promise { 37 | try { 38 | return await this.executor.execute(params) 39 | } catch (error) { 40 | const err = error as Error 41 | throw this.createError(err.message, (err as unknown as { status?: number }).status) 42 | } 43 | } 44 | 45 | /** 46 | * Creates a streaming response from Claude CLI 47 | */ 48 | protected executeStreamCommand(params: ClaudeExecParams): NodeJS.ReadableStream { 49 | return this.executor.executeStream(params) 50 | } 51 | } -------------------------------------------------------------------------------- /typescript/src/client/chat.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream' 2 | import { BaseClient } from './base' 3 | import { 4 | OpenAIChatCompletion, 5 | OpenAIChatCompletionCreateParams, 6 | OpenAIChatCompletionChunk, 7 | } from '../types' 8 | import { 9 | convertMessagesToPrompt, 10 | parseCliOutput, 11 | convertOpenAIToAnthropicTools, 12 | } from '../implementations/converters' 13 | 14 | // Class for OpenAI-style completions 15 | export class ChatCompletions { 16 | private client: BaseClient 17 | 18 | constructor(client: BaseClient) { 19 | this.client = client 20 | } 21 | 22 | /** 23 | * Create a completion (OpenAI style) 24 | */ 25 | async create(params: OpenAIChatCompletionCreateParams): Promise { 26 | // Convert the OpenAI-style parameters to CLI parameters 27 | const prompt = convertMessagesToPrompt(params.messages) 28 | 29 | const cliParams = { 30 | prompt, 31 | outputFormat: 'json' as const, 32 | temperature: params.temperature, 33 | maxTokens: params.max_tokens, 34 | topP: params.top_p, 35 | stop: params.stop ? 36 | (Array.isArray(params.stop) ? params.stop.join(',') : params.stop) : 37 | undefined, 38 | timeout: params.timeout, 39 | } 40 | 41 | // Handle tools if provided 42 | if (params.tools && params.tools.length > 0) { 43 | const anthropicTools = convertOpenAIToAnthropicTools(params.tools) 44 | const toolNames = anthropicTools.map((tool) => tool.name) 45 | // Use index signature to add allowedTools property 46 | ;(cliParams as { allowedTools?: string[] }).allowedTools = toolNames 47 | } 48 | 49 | if (params.stream) { 50 | // Create streaming response 51 | return this.createStream(params) as unknown as OpenAIChatCompletion 52 | } else { 53 | // Execute and parse response 54 | const output = await this.client['executeCommand'](cliParams) 55 | return parseCliOutput(output) 56 | } 57 | } 58 | 59 | /** 60 | * Create a streaming completion (OpenAI style) 61 | */ 62 | createStream(params: OpenAIChatCompletionCreateParams): AsyncIterable { 63 | // Convert the OpenAI-style parameters to CLI parameters 64 | const prompt = convertMessagesToPrompt(params.messages) 65 | 66 | const cliParams = { 67 | prompt, 68 | outputFormat: 'stream-json' as const, 69 | temperature: params.temperature, 70 | maxTokens: params.max_tokens, 71 | topP: params.top_p, 72 | stop: params.stop ? 73 | (Array.isArray(params.stop) ? params.stop.join(',') : params.stop) : 74 | undefined, 75 | timeout: params.timeout, 76 | } 77 | 78 | // Handle tools if provided 79 | if (params.tools && params.tools.length > 0) { 80 | const anthropicTools = convertOpenAIToAnthropicTools(params.tools) 81 | const toolNames = anthropicTools.map((tool) => tool.name) 82 | // Use index signature to add allowedTools property 83 | ;(cliParams as { allowedTools?: string[] }).allowedTools = toolNames 84 | } 85 | 86 | // Get streaming response 87 | const stream = this.client['executeStreamCommand'](cliParams) as Readable 88 | 89 | // Create an async iterator from the stream 90 | const asyncIterator: AsyncIterable = { 91 | [Symbol.asyncIterator]() { 92 | return { 93 | next(): Promise> { 94 | return new Promise((resolve, reject) => { 95 | stream.once('data', (data: Buffer) => { 96 | try { 97 | // Parse each chunk as a separate JSON object 98 | const chunkStr = data.toString().trim() 99 | 100 | // Handle potential multiple JSON objects in one chunk 101 | const jsonStrings = chunkStr 102 | .split('\n') 103 | .filter(s => s.trim().length > 0) 104 | 105 | for (const jsonStr of jsonStrings) { 106 | try { 107 | const chunk = JSON.parse(jsonStr) as OpenAIChatCompletionChunk 108 | resolve({ done: false, value: chunk }) 109 | return 110 | } catch (e) { 111 | // Skip invalid JSON 112 | console.warn('Invalid JSON in stream chunk:', jsonStr) 113 | } 114 | } 115 | 116 | // If we couldn't parse anything, resolve with empty chunk 117 | resolve({ done: false, value: {} as OpenAIChatCompletionChunk }) 118 | } catch (error) { 119 | reject(error) 120 | } 121 | }) 122 | 123 | stream.once('end', () => { 124 | resolve({ done: true, value: undefined }) 125 | }) 126 | 127 | stream.once('error', (error) => { 128 | reject(error) 129 | }) 130 | }) 131 | } 132 | } 133 | } 134 | } 135 | 136 | return asyncIterator 137 | } 138 | 139 | /** 140 | * Batch create completions (custom extension) 141 | */ 142 | async batchCreate(params: OpenAIChatCompletionCreateParams[]): Promise { 143 | return Promise.all(params.map(p => this.create(p))) 144 | } 145 | } -------------------------------------------------------------------------------- /typescript/src/client/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseClient } from './base' 2 | import { ChatCompletions } from './chat' 3 | import { Messages } from './messages' 4 | import { Sessions } from './sessions' 5 | import { Tools } from './tools' 6 | import { ClaudeCodeOptions } from '../types' 7 | 8 | // Main client class 9 | export class ClaudeCode extends BaseClient { 10 | readonly chat: { completions: ChatCompletions } 11 | readonly messages: Messages 12 | readonly sessions: Sessions 13 | readonly tools: Tools 14 | 15 | constructor(options: ClaudeCodeOptions = {}) { 16 | super(options) 17 | 18 | // Initialize OpenAI-style chat completions 19 | const completionsInstance = new ChatCompletions(this) 20 | this.chat = { 21 | completions: completionsInstance, 22 | } 23 | 24 | // Initialize Anthropic-style messages 25 | this.messages = new Messages(this) 26 | 27 | // Initialize sessions 28 | this.sessions = new Sessions(this) 29 | 30 | // Initialize tools 31 | this.tools = new Tools(this) 32 | } 33 | 34 | // Convenience method for configuring MCP server URL 35 | setMcpServer(url: string): void { 36 | process.env.CLAUDE_CODE_MCP_URL = url 37 | } 38 | } -------------------------------------------------------------------------------- /typescript/src/client/messages.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream' 2 | import { BaseClient } from './base' 3 | import { 4 | AnthropicMessageResponse, 5 | AnthropicMessageCreateParams, 6 | AnthropicMessageStreamPart, 7 | } from '../types' 8 | import { 9 | convertMessagesToPrompt, 10 | parseCliOutput, 11 | } from '../implementations/converters' 12 | 13 | // Class for Anthropic-style messages 14 | export class Messages { 15 | private client: BaseClient 16 | 17 | constructor(client: BaseClient) { 18 | this.client = client 19 | } 20 | 21 | /** 22 | * Create a message (Anthropic style) 23 | */ 24 | async create(params: AnthropicMessageCreateParams): Promise { 25 | // Convert the Anthropic-style parameters to CLI parameters 26 | const prompt = convertMessagesToPrompt(params.messages) 27 | 28 | const cliParams = { 29 | prompt, 30 | outputFormat: 'json' as const, 31 | temperature: params.temperature, 32 | maxTokens: params.max_tokens, 33 | topP: params.top_p, 34 | stop: params.stop_sequences ? params.stop_sequences.join(',') : undefined, 35 | timeout: params.timeout, 36 | } 37 | 38 | // Handle tools if provided 39 | if (params.tools && params.tools.length > 0) { 40 | const toolNames = params.tools.map((tool) => tool.name) 41 | // Use index signature to add allowedTools property 42 | ;(cliParams as { allowedTools?: string[] }).allowedTools = toolNames 43 | } 44 | 45 | if (params.stream) { 46 | // Create streaming response 47 | return this.createStream(params) as unknown as AnthropicMessageResponse 48 | } else { 49 | // Execute and parse response 50 | const output = await this.client['executeCommand'](cliParams) 51 | return parseCliOutput(output) 52 | } 53 | } 54 | 55 | /** 56 | * Create a streaming message (Anthropic style) 57 | */ 58 | createStream(params: AnthropicMessageCreateParams): AsyncIterable { 59 | // Convert the Anthropic-style parameters to CLI parameters 60 | const prompt = convertMessagesToPrompt(params.messages) 61 | 62 | const cliParams = { 63 | prompt, 64 | outputFormat: 'stream-json' as const, 65 | temperature: params.temperature, 66 | maxTokens: params.max_tokens, 67 | topP: params.top_p, 68 | stop: params.stop_sequences ? params.stop_sequences.join(',') : undefined, 69 | timeout: params.timeout, 70 | } 71 | 72 | // Handle tools if provided 73 | if (params.tools && params.tools.length > 0) { 74 | const toolNames = params.tools.map((tool) => tool.name) 75 | // Use index signature to add allowedTools property 76 | ;(cliParams as { allowedTools?: string[] }).allowedTools = toolNames 77 | } 78 | 79 | // Get streaming response 80 | const stream = this.client['executeStreamCommand'](cliParams) as Readable 81 | 82 | // Create an async iterator from the stream 83 | const asyncIterator: AsyncIterable = { 84 | [Symbol.asyncIterator]() { 85 | return { 86 | next(): Promise> { 87 | return new Promise((resolve, reject) => { 88 | stream.once('data', (data: Buffer) => { 89 | try { 90 | // Parse each chunk as a separate JSON object 91 | const chunkStr = data.toString().trim() 92 | 93 | // Handle potential multiple JSON objects in one chunk 94 | const jsonStrings = chunkStr 95 | .split('\n') 96 | .filter(s => s.trim().length > 0) 97 | 98 | for (const jsonStr of jsonStrings) { 99 | try { 100 | const chunk = JSON.parse(jsonStr) as AnthropicMessageStreamPart 101 | resolve({ done: false, value: chunk }) 102 | return 103 | } catch (e) { 104 | // Skip invalid JSON 105 | console.warn('Invalid JSON in stream chunk:', jsonStr) 106 | } 107 | } 108 | 109 | // If we couldn't parse anything, resolve with empty chunk 110 | resolve({ done: false, value: {} as AnthropicMessageStreamPart }) 111 | } catch (error) { 112 | reject(error) 113 | } 114 | }) 115 | 116 | stream.once('end', () => { 117 | resolve({ done: true, value: undefined }) 118 | }) 119 | 120 | stream.once('error', (error) => { 121 | reject(error) 122 | }) 123 | }) 124 | } 125 | } 126 | } 127 | } 128 | 129 | return asyncIterator 130 | } 131 | 132 | /** 133 | * Batch create messages (custom extension) 134 | */ 135 | async batchCreate(params: AnthropicMessageCreateParams[]): Promise { 136 | return Promise.all(params.map(p => this.create(p))) 137 | } 138 | } -------------------------------------------------------------------------------- /typescript/src/client/sessions.ts: -------------------------------------------------------------------------------- 1 | import { BaseClient } from './base' 2 | import { 3 | OpenAIMessage, 4 | AnthropicMessage, 5 | OpenAIChatCompletion, 6 | AnthropicMessageResponse, 7 | SessionParams, 8 | SessionContinueParams, 9 | } from '../types' 10 | import { 11 | convertMessagesToPrompt, 12 | parseCliOutput, 13 | } from '../implementations/converters' 14 | 15 | // Class for session management 16 | export class Sessions { 17 | private client: BaseClient 18 | 19 | constructor(client: BaseClient) { 20 | this.client = client 21 | } 22 | 23 | /** 24 | * Create a new session 25 | */ 26 | async create(params: SessionParams): Promise { 27 | // Convert messages to prompt 28 | const prompt = convertMessagesToPrompt(params.messages) 29 | 30 | // Start a new session 31 | const cliParams = { 32 | prompt, 33 | outputFormat: 'json' as const, 34 | } 35 | 36 | // Execute command and get initial response 37 | const output = await this.client['executeCommand'](cliParams) 38 | const response = parseCliOutput>(output) 39 | 40 | // Extract session ID if present 41 | const sessionId = (response as { id?: string }).id || 42 | `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` 43 | 44 | // Create and return a new session 45 | return new Session(sessionId, this.client, params.messages) 46 | } 47 | 48 | /** 49 | * Resume an existing session by ID 50 | */ 51 | async resume(sessionId: string): Promise { 52 | // Resume a session by ID 53 | const cliParams = { 54 | resume: sessionId, 55 | outputFormat: 'json' as const, 56 | } 57 | 58 | // Execute command to resume session 59 | await this.client['executeCommand'](cliParams) 60 | 61 | // Create and return session object 62 | return new Session(sessionId, this.client, []) 63 | } 64 | } 65 | 66 | // Individual session class 67 | export class Session { 68 | readonly id: string 69 | private client: BaseClient 70 | private messages: Array 71 | 72 | constructor(id: string, client: BaseClient, messages: Array) { 73 | this.id = id 74 | this.client = client 75 | this.messages = [...messages] 76 | } 77 | 78 | /** 79 | * Continue a session with additional messages 80 | */ 81 | async continue(params: SessionContinueParams): Promise { 82 | // Add new messages to the existing ones 83 | this.messages = [...this.messages, ...params.messages] 84 | 85 | // Convert messages to prompt 86 | const prompt = convertMessagesToPrompt(params.messages) 87 | 88 | // Continue the session 89 | const cliParams = { 90 | prompt, 91 | resume: this.id, 92 | outputFormat: 'json' as const, 93 | } 94 | 95 | // Execute command and get response 96 | const output = await this.client['executeCommand'](cliParams) 97 | return parseCliOutput(output) 98 | } 99 | 100 | /** 101 | * Get all messages in this session 102 | */ 103 | getMessages(): Array { 104 | return [...this.messages] 105 | } 106 | } -------------------------------------------------------------------------------- /typescript/src/client/tools.ts: -------------------------------------------------------------------------------- 1 | import { BaseClient } from './base' 2 | import { AnthropicTool } from '../types' 3 | 4 | // Interface for tool creation 5 | export interface ToolCreateParams { 6 | name: string 7 | description?: string 8 | input_schema: Record 9 | } 10 | 11 | // Class for tools management 12 | export class Tools { 13 | private client: BaseClient 14 | private registeredTools: Map 15 | 16 | constructor(client: BaseClient) { 17 | this.client = client 18 | this.registeredTools = new Map() 19 | } 20 | 21 | /** 22 | * Register a new tool for later use 23 | */ 24 | async create(params: ToolCreateParams): Promise { 25 | // Create a new tool definition 26 | const tool: AnthropicTool = { 27 | name: params.name, 28 | description: params.description, 29 | input_schema: params.input_schema, 30 | } 31 | 32 | // Store the tool for later use 33 | this.registeredTools.set(params.name, tool) 34 | 35 | return tool 36 | } 37 | 38 | /** 39 | * Get a tool by name 40 | */ 41 | get(name: string): AnthropicTool | undefined { 42 | return this.registeredTools.get(name) 43 | } 44 | 45 | /** 46 | * List all registered tools 47 | */ 48 | list(): AnthropicTool[] { 49 | return Array.from(this.registeredTools.values()) 50 | } 51 | } -------------------------------------------------------------------------------- /typescript/src/examples/basic.ts: -------------------------------------------------------------------------------- 1 | import { ClaudeCode } from '../index' 2 | 3 | // Example usage of the Claude Code SDK 4 | 5 | async function main(): Promise { 6 | // Create a client 7 | const claude = new ClaudeCode({ 8 | apiKey: process.env.ANTHROPIC_API_KEY, 9 | }) 10 | 11 | try { 12 | console.log('Using OpenAI-style chat completions API:') 13 | const completion = await claude.chat.completions.create({ 14 | model: 'claude-code', 15 | messages: [ 16 | { role: 'user', content: 'Write a function to calculate the Fibonacci sequence in TypeScript' } 17 | ], 18 | max_tokens: 1000, 19 | }) 20 | 21 | console.log(completion.choices[0].message.content) 22 | console.log('\n---\n') 23 | 24 | console.log('Using Anthropic-style messages API:') 25 | const message = await claude.messages.create({ 26 | model: 'claude-code', 27 | messages: [ 28 | { 29 | role: 'user', 30 | content: [{ 31 | type: 'text', 32 | text: 'Write a function to sort an array using quicksort in TypeScript' 33 | }] 34 | } 35 | ], 36 | max_tokens: 1000, 37 | }) 38 | 39 | if (Array.isArray(message.content)) { 40 | console.log(message.content[0].text) 41 | } 42 | console.log('\n---\n') 43 | 44 | console.log('Using session management:') 45 | const session = await claude.sessions.create({ 46 | messages: [ 47 | { role: 'user', content: 'Let\'s create a simple Express.js API' } 48 | ] 49 | }) 50 | 51 | console.log('Session created with ID:', session.id) 52 | 53 | const sessionResponse = await session.continue({ 54 | messages: [ 55 | { role: 'user', content: 'Now add a route to get a list of users' } 56 | ] 57 | }) 58 | 59 | console.log('Session response:') 60 | // Handle both OpenAI and Anthropic response formats 61 | if ('choices' in sessionResponse) { 62 | console.log(sessionResponse.choices[0].message.content) 63 | } else if ('content' in sessionResponse && Array.isArray(sessionResponse.content)) { 64 | console.log(sessionResponse.content[0].text) 65 | } 66 | 67 | } catch (error) { 68 | console.error('Error:', error) 69 | } 70 | } 71 | 72 | // Execute the example if this file is run directly 73 | if (require.main === module) { 74 | main().catch(console.error) 75 | } 76 | 77 | export default main -------------------------------------------------------------------------------- /typescript/src/examples/streaming.ts: -------------------------------------------------------------------------------- 1 | import { ClaudeCode } from '../index' 2 | 3 | // Example of streaming responses with Claude Code SDK 4 | 5 | async function streamExample(): Promise { 6 | // Create a client 7 | const claude = new ClaudeCode({ 8 | apiKey: process.env.ANTHROPIC_API_KEY, 9 | }) 10 | 11 | try { 12 | console.log('Streaming with OpenAI-style API:') 13 | console.log('--------------------------------') 14 | 15 | // OpenAI-style streaming 16 | const openaiStream = await claude.chat.completions.createStream({ 17 | model: 'claude-code', 18 | messages: [ 19 | { role: 'user', content: 'Write a React component for a to-do list with TypeScript' } 20 | ] 21 | }) 22 | 23 | console.log('Response:') 24 | for await (const chunk of openaiStream) { 25 | if (chunk.choices && chunk.choices[0]?.delta?.content) { 26 | process.stdout.write(chunk.choices[0].delta.content) 27 | } 28 | } 29 | 30 | console.log('\n\n') 31 | console.log('Streaming with Anthropic-style API:') 32 | console.log('---------------------------------') 33 | 34 | // Anthropic-style streaming 35 | const anthropicStream = await claude.messages.createStream({ 36 | model: 'claude-code', 37 | messages: [ 38 | { 39 | role: 'user', 40 | content: [{ 41 | type: 'text', 42 | text: 'Write a TypeScript utility for handling API errors' 43 | }] 44 | } 45 | ] 46 | }) 47 | 48 | console.log('Response:') 49 | for await (const chunk of anthropicStream) { 50 | if (chunk.type === 'content_block_delta' && chunk.delta?.text) { 51 | process.stdout.write(chunk.delta.text) 52 | } 53 | } 54 | 55 | console.log('\n') 56 | 57 | } catch (error) { 58 | console.error('Error:', error) 59 | } 60 | } 61 | 62 | // Execute the example if this file is run directly 63 | if (require.main === module) { 64 | streamExample().catch(console.error) 65 | } 66 | 67 | export default streamExample -------------------------------------------------------------------------------- /typescript/src/implementations/cli.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import { promisify } from 'util' 3 | import { Readable } from 'stream' 4 | 5 | const execP = promisify(exec) 6 | 7 | export interface ClaudeExecOptions { 8 | cliPath?: string 9 | timeout?: number 10 | env?: NodeJS.ProcessEnv 11 | } 12 | 13 | export type OutputFormat = 'text' | 'json' | 'stream-json' 14 | 15 | export interface ClaudeExecParams { 16 | prompt?: string 17 | outputFormat?: OutputFormat 18 | systemPrompt?: string 19 | continue?: boolean 20 | resume?: string 21 | allowedTools?: string[] 22 | disallowedTools?: string[] 23 | mcpConfig?: string 24 | maxTurns?: number 25 | maxTokens?: number 26 | temperature?: number 27 | topP?: number 28 | stop?: string 29 | timeout?: number 30 | [key: string]: unknown 31 | } 32 | 33 | export class ClaudeCliExecutor { 34 | private cliPath: string 35 | private defaultTimeout: number 36 | private env: NodeJS.ProcessEnv 37 | 38 | constructor(options: ClaudeExecOptions = {}) { 39 | this.cliPath = options.cliPath || '@anthropic-ai/claude-code' 40 | this.defaultTimeout = options.timeout || 300000 // 5 minutes default 41 | this.env = { ...process.env, ...(options.env || {}) } 42 | } 43 | 44 | /** 45 | * Builds arguments array for the Claude CLI based on provided parameters 46 | */ 47 | private buildArgs(params: ClaudeExecParams): string[] { 48 | const args: string[] = [] 49 | 50 | if (params.prompt) { 51 | args.push('-p') 52 | args.push(params.prompt) 53 | } 54 | 55 | if (params.outputFormat) { 56 | args.push('--output-format') 57 | args.push(params.outputFormat) 58 | } 59 | 60 | if (params.systemPrompt) { 61 | args.push('--system-prompt') 62 | args.push(params.systemPrompt) 63 | } 64 | 65 | if (params.continue) { 66 | args.push('--continue') 67 | } 68 | 69 | if (params.resume) { 70 | args.push('--resume') 71 | args.push(params.resume) 72 | } 73 | 74 | if (params.allowedTools && params.allowedTools.length > 0) { 75 | args.push('--allowedTools') 76 | args.push(params.allowedTools.join(',')) 77 | } 78 | 79 | if (params.disallowedTools && params.disallowedTools.length > 0) { 80 | args.push('--disallowedTools') 81 | args.push(params.disallowedTools.join(',')) 82 | } 83 | 84 | if (params.mcpConfig) { 85 | args.push('--mcp-config') 86 | args.push(params.mcpConfig) 87 | } 88 | 89 | if (params.maxTurns) { 90 | args.push('--max-turns') 91 | args.push(String(params.maxTurns)) 92 | } 93 | 94 | // Add any additional parameters provided 95 | for (const [key, value] of Object.entries(params)) { 96 | if ( 97 | !['prompt', 'outputFormat', 'systemPrompt', 'continue', 'resume', 'allowedTools', 98 | 'disallowedTools', 'mcpConfig', 'maxTurns'].includes(key) && 99 | value !== undefined 100 | ) { 101 | // Convert camelCase to kebab-case for CLI flags 102 | const kebabKey = key.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() 103 | args.push(`--${kebabKey}`) 104 | args.push(String(value)) 105 | } 106 | } 107 | 108 | return args 109 | } 110 | 111 | /** 112 | * Execute a Claude CLI command and return the result 113 | */ 114 | public async execute(params: ClaudeExecParams, timeout?: number): Promise { 115 | const args = this.buildArgs(params) 116 | const timeoutMs = timeout || this.defaultTimeout 117 | 118 | return new Promise((resolve, reject) => { 119 | // Use spawn for better handling of arguments 120 | const { spawn } = require('child_process') 121 | const childProcess = spawn(this.cliPath, args, { 122 | env: this.env, 123 | }) 124 | 125 | let stdout = '' 126 | let stderr = '' 127 | 128 | // Set timeout 129 | const timeoutId = setTimeout(() => { 130 | childProcess.kill() 131 | const error = new Error(`Claude CLI execution timed out after ${timeoutMs}ms`) as Error & { status?: number; code?: string } 132 | error.status = 408 133 | reject(error) 134 | }, timeoutMs) 135 | 136 | childProcess.stdout.on('data', (data: Buffer) => { 137 | stdout += String(data) 138 | }) 139 | 140 | childProcess.stderr.on('data', (data: Buffer) => { 141 | stderr += String(data) 142 | }) 143 | 144 | childProcess.on('error', (error: Error) => { 145 | clearTimeout(timeoutId) 146 | 147 | // Create a more informative error 148 | const enhancedError = new Error( 149 | `Claude CLI execution failed: ${error.message}${stderr ? `\nStderr: ${stderr}` : ''}` 150 | ) as Error & { status?: number; code?: string } 151 | 152 | // Add status code for OpenAI/Anthropic compatibility 153 | enhancedError.status = 500 154 | 155 | reject(enhancedError) 156 | }) 157 | 158 | childProcess.on('close', (code: number) => { 159 | clearTimeout(timeoutId) 160 | 161 | if (code !== 0) { 162 | // Create a more informative error 163 | const enhancedError = new Error( 164 | `Claude CLI process exited with code ${code}${stderr ? `\nStderr: ${stderr}` : ''}` 165 | ) as Error & { status?: number; code?: string } 166 | 167 | // Add status code for OpenAI/Anthropic compatibility 168 | enhancedError.status = code || 500 169 | 170 | reject(enhancedError) 171 | } else { 172 | if (stderr) { 173 | console.error('Claude CLI stderr:', stderr) 174 | } 175 | 176 | resolve(stdout) 177 | } 178 | }) 179 | }) 180 | } 181 | 182 | /** 183 | * Execute a Claude CLI command in streaming mode and return a readable stream 184 | */ 185 | public executeStream(params: ClaudeExecParams): Readable { 186 | // Ensure we use stream-json format for streaming 187 | const streamParams = { ...params, outputFormat: 'stream-json' as const } 188 | 189 | // Build the arguments array 190 | const args = this.buildArgs(streamParams) 191 | 192 | // Create a child process for streaming 193 | const { spawn } = require('child_process') 194 | const childProcess = spawn(this.cliPath, args, { env: this.env }) 195 | 196 | // Create a readable stream to return to the caller 197 | const outputStream = new Readable({ 198 | read() {} // Implementation required but not used 199 | }) 200 | 201 | // Handle data events 202 | childProcess.stdout.on('data', (data: Buffer) => { 203 | outputStream.push(data) 204 | }) 205 | 206 | // Handle errors and close events 207 | childProcess.stderr.on('data', (data: Buffer) => { 208 | console.error('Claude CLI Stream stderr:', String(data)) 209 | }) 210 | 211 | childProcess.on('error', (error: Error) => { 212 | outputStream.emit('error', error) 213 | outputStream.push(null) // End the stream 214 | }) 215 | 216 | childProcess.on('close', (code: number) => { 217 | if (code !== 0) { 218 | outputStream.emit('error', new Error(`Claude CLI process exited with code ${code}`)) 219 | } 220 | outputStream.push(null) // End the stream 221 | }) 222 | 223 | return outputStream 224 | } 225 | } -------------------------------------------------------------------------------- /typescript/src/implementations/converters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OpenAIMessage, 3 | AnthropicMessage, 4 | ContentBlock, 5 | OpenAITool, 6 | AnthropicTool, 7 | } from '../types' 8 | 9 | /** 10 | * Converts an OpenAI style message to an Anthropic style message 11 | */ 12 | export function convertOpenAIToAnthropicMessage(message: OpenAIMessage): AnthropicMessage { 13 | if (typeof message.content === 'string') { 14 | return { 15 | role: message.role, 16 | content: [{ type: 'text', text: message.content }], 17 | files: message.files, 18 | } 19 | } else { 20 | // Message content is already in a compatible format 21 | return message as unknown as AnthropicMessage 22 | } 23 | } 24 | 25 | /** 26 | * Converts an Anthropic style message to an OpenAI style message 27 | */ 28 | export function convertAnthropicToOpenAIMessage(message: AnthropicMessage): OpenAIMessage { 29 | if (typeof message.content === 'string') { 30 | return message as unknown as OpenAIMessage 31 | } else { 32 | // Convert content array to string (concatenating text blocks) 33 | const contentBlocks = message.content as ContentBlock[] 34 | const textContent = contentBlocks 35 | .filter(block => block.type === 'text' && block.text) 36 | .map(block => block.text) 37 | .join('\n') 38 | 39 | return { 40 | role: message.role, 41 | content: textContent, 42 | files: message.files, 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Converts OpenAI style tools to Anthropic style tools 49 | */ 50 | export function convertOpenAIToAnthropicTools(tools: OpenAITool[]): AnthropicTool[] { 51 | return tools.map(tool => { 52 | return { 53 | name: tool.function.name, 54 | description: tool.function.description, 55 | input_schema: tool.function.parameters || {}, 56 | } 57 | }) 58 | } 59 | 60 | /** 61 | * Converts Anthropic style tools to OpenAI style tools 62 | */ 63 | export function convertAnthropicToOpenAITools(tools: AnthropicTool[]): OpenAITool[] { 64 | return tools.map(tool => { 65 | return { 66 | type: 'function', 67 | function: { 68 | name: tool.name, 69 | description: tool.description, 70 | parameters: tool.input_schema, 71 | }, 72 | } 73 | }) 74 | } 75 | 76 | /** 77 | * Converts an array of messages to a single prompt string for the CLI 78 | */ 79 | export function convertMessagesToPrompt(messages: Array): string { 80 | // Handle the case where we might receive either OpenAI or Anthropic style messages 81 | return messages 82 | .map(message => { 83 | const role = message.role.toUpperCase() 84 | 85 | // Handle different content formats 86 | let content: string 87 | if (typeof message.content === 'string') { 88 | content = message.content 89 | } else { 90 | // Convert Anthropic's content array to string 91 | content = (message.content as ContentBlock[]) 92 | .filter(block => block.type === 'text' && block.text) 93 | .map(block => block.text) 94 | .join('\n') 95 | } 96 | 97 | return `${role}: ${content}` 98 | }) 99 | .join('\n\n') 100 | } 101 | 102 | /** 103 | * Parses CLI output in JSON format to appropriate completion response 104 | */ 105 | export function parseCliOutput(output: string): T { 106 | try { 107 | return JSON.parse(output) as T 108 | } catch (error) { 109 | // If we can't parse as JSON, return text output in a structured format 110 | return { 111 | choices: [ 112 | { 113 | message: { 114 | role: 'assistant', 115 | content: output.trim(), 116 | }, 117 | }, 118 | ], 119 | } as unknown as T 120 | } 121 | } -------------------------------------------------------------------------------- /typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | // Export main client 2 | export { ClaudeCode } from './client' 3 | 4 | // Export types 5 | export * from './types' 6 | 7 | // Export session classes 8 | export { Session } from './client/sessions' 9 | 10 | // Create default export for convenience 11 | import { ClaudeCode } from './client' 12 | export default ClaudeCode -------------------------------------------------------------------------------- /typescript/src/tests/cli.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from 'vitest' 2 | import { ClaudeCliExecutor } from '../implementations/cli' 3 | import { Readable } from 'stream' 4 | 5 | describe('ClaudeCliExecutor', () => { 6 | describe('buildArgs', () => { 7 | it('should build arguments array with the correct parameters', () => { 8 | const executor = new ClaudeCliExecutor({ cliPath: 'claude-code' }) 9 | 10 | // Access the private method for testing 11 | const buildArgs = (executor as any)['buildArgs'].bind(executor) 12 | 13 | // Test basic parameters 14 | const args1 = buildArgs({ 15 | prompt: 'Test prompt', 16 | outputFormat: 'json' 17 | }) 18 | 19 | expect(args1).toContain('-p') 20 | expect(args1).toContain('Test prompt') 21 | expect(args1).toContain('--output-format') 22 | expect(args1).toContain('json') 23 | 24 | // Test with system prompt 25 | const args2 = buildArgs({ 26 | prompt: 'Test prompt', 27 | systemPrompt: 'You are a helpful assistant', 28 | outputFormat: 'json' 29 | }) 30 | 31 | expect(args2).toContain('--system-prompt') 32 | expect(args2).toContain('You are a helpful assistant') 33 | 34 | // Test with quote escaping (quotes are not escaped when using spawn with args) 35 | const args3 = buildArgs({ 36 | prompt: 'Test "quoted" prompt', 37 | outputFormat: 'json' 38 | }) 39 | 40 | expect(args3).toContain('Test "quoted" prompt') 41 | 42 | // Test with allowed tools 43 | const args4 = buildArgs({ 44 | prompt: 'Test prompt', 45 | allowedTools: ['filesystem', 'web-search'] 46 | }) 47 | 48 | expect(args4).toContain('--allowedTools') 49 | expect(args4).toContain('filesystem,web-search') 50 | 51 | // Test with session continuation 52 | const args5 = buildArgs({ 53 | continue: true, 54 | prompt: 'Continue session' 55 | }) 56 | 57 | expect(args5).toContain('--continue') 58 | 59 | // Test with session resumption 60 | const args6 = buildArgs({ 61 | resume: 'abc123', 62 | prompt: 'Resume session' 63 | }) 64 | 65 | expect(args6).toContain('--resume') 66 | expect(args6).toContain('abc123') 67 | }) 68 | }) 69 | }) -------------------------------------------------------------------------------- /typescript/src/tests/client.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from 'vitest' 2 | import { ClaudeCode } from '../index' 3 | import { ClaudeCliExecutor } from '../implementations/cli' 4 | 5 | // Mock the CLI executor class 6 | vi.mock('../implementations/cli', () => { 7 | return { 8 | ClaudeCliExecutor: vi.fn().mockImplementation(() => { 9 | return { 10 | execute: vi.fn().mockResolvedValue('{"id": "mock-id", "choices": [{"message": {"role": "assistant", "content": "Mock response"}}]}'), 11 | executeStream: vi.fn(() => { 12 | const { Readable } = require('stream') 13 | const stream = new Readable({ 14 | read() {} 15 | }) 16 | 17 | // Simulate data events 18 | setTimeout(() => { 19 | stream.push(Buffer.from('{"id": "chunk-1", "choices": [{"delta": {"content": "Mock"}}]}')) 20 | stream.push(Buffer.from('{"id": "chunk-2", "choices": [{"delta": {"content": " response"}}]}')) 21 | stream.push(null) // End the stream 22 | }, 0) 23 | 24 | return stream 25 | }) 26 | } 27 | }) 28 | } 29 | }) 30 | 31 | describe('ClaudeCode client', () => { 32 | let claude: ClaudeCode 33 | 34 | beforeEach(() => { 35 | claude = new ClaudeCode({ apiKey: 'test-api-key' }) 36 | }) 37 | 38 | describe('Client initialization', () => { 39 | it('should create a client instance', () => { 40 | expect(claude).toBeInstanceOf(ClaudeCode) 41 | expect(claude.chat.completions).toBeDefined() 42 | expect(claude.messages).toBeDefined() 43 | expect(claude.sessions).toBeDefined() 44 | expect(claude.tools).toBeDefined() 45 | }) 46 | 47 | it('should initialize with provided options', () => { 48 | const customClient = new ClaudeCode({ 49 | apiKey: 'custom-key', 50 | cliPath: 'custom-path', 51 | timeout: 60000, 52 | }) 53 | 54 | expect(customClient).toBeInstanceOf(ClaudeCode) 55 | }) 56 | }) 57 | 58 | describe('OpenAI-style API', () => { 59 | it('should have chat.completions.create method', () => { 60 | expect(typeof claude.chat.completions.create).toBe('function') 61 | }) 62 | 63 | it('should have chat.completions.createStream method', () => { 64 | expect(typeof claude.chat.completions.createStream).toBe('function') 65 | }) 66 | 67 | it('should call the CLI executor when using create', async () => { 68 | const response = await claude.chat.completions.create({ 69 | model: 'claude-code', 70 | messages: [{ role: 'user', content: 'Test prompt' }] 71 | }) 72 | 73 | expect(response).toBeDefined() 74 | expect(response.id).toBe('mock-id') 75 | expect(response.choices[0].message.content).toBe('Mock response') 76 | }) 77 | }) 78 | 79 | describe('Anthropic-style API', () => { 80 | it('should have messages.create method', () => { 81 | expect(typeof claude.messages.create).toBe('function') 82 | }) 83 | 84 | it('should have messages.createStream method', () => { 85 | expect(typeof claude.messages.createStream).toBe('function') 86 | }) 87 | 88 | it('should call the CLI executor when using create', async () => { 89 | const response = await claude.messages.create({ 90 | model: 'claude-code', 91 | messages: [{ 92 | role: 'user', 93 | content: [{ type: 'text', text: 'Test prompt' }] 94 | }] 95 | }) 96 | 97 | expect(response).toBeDefined() 98 | }) 99 | }) 100 | 101 | describe('Session management', () => { 102 | it('should have sessions.create method', () => { 103 | expect(typeof claude.sessions.create).toBe('function') 104 | }) 105 | 106 | it('should have sessions.resume method', () => { 107 | expect(typeof claude.sessions.resume).toBe('function') 108 | }) 109 | 110 | it('should create a session with an ID', async () => { 111 | const session = await claude.sessions.create({ 112 | messages: [{ role: 'user', content: 'Start session' }] 113 | }) 114 | 115 | expect(session).toBeDefined() 116 | expect(session.id).toBeDefined() 117 | }) 118 | }) 119 | 120 | describe('Tools', () => { 121 | it('should have tools.create method', () => { 122 | expect(typeof claude.tools.create).toBe('function') 123 | }) 124 | 125 | it('should have tools.get method', () => { 126 | expect(typeof claude.tools.get).toBe('function') 127 | }) 128 | 129 | it('should have tools.list method', () => { 130 | expect(typeof claude.tools.list).toBe('function') 131 | }) 132 | 133 | it('should register a tool and retrieve it', async () => { 134 | const tool = await claude.tools.create({ 135 | name: 'test-tool', 136 | description: 'A test tool', 137 | input_schema: { type: 'object', properties: { test: { type: 'string' } } } 138 | }) 139 | 140 | expect(tool).toBeDefined() 141 | expect(tool.name).toBe('test-tool') 142 | 143 | const retrievedTool = claude.tools.get('test-tool') 144 | expect(retrievedTool).toBeDefined() 145 | expect(retrievedTool?.name).toBe('test-tool') 146 | 147 | const allTools = claude.tools.list() 148 | expect(allTools).toHaveLength(1) 149 | expect(allTools[0].name).toBe('test-tool') 150 | }) 151 | }) 152 | }) -------------------------------------------------------------------------------- /typescript/src/tests/converters.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { 3 | convertOpenAIToAnthropicMessage, 4 | convertAnthropicToOpenAIMessage, 5 | convertOpenAIToAnthropicTools, 6 | convertAnthropicToOpenAITools, 7 | convertMessagesToPrompt, 8 | parseCliOutput 9 | } from '../implementations/converters' 10 | import { AnthropicMessage, OpenAIMessage, OpenAITool, AnthropicTool } from '../types' 11 | 12 | describe('Converter utilities', () => { 13 | describe('convertOpenAIToAnthropicMessage', () => { 14 | it('should convert OpenAI message to Anthropic format', () => { 15 | const openAIMessage: OpenAIMessage = { 16 | role: 'user', 17 | content: 'Test message' 18 | } 19 | 20 | const anthropicMessage = convertOpenAIToAnthropicMessage(openAIMessage) 21 | 22 | expect(anthropicMessage.role).toBe('user') 23 | expect(Array.isArray(anthropicMessage.content)).toBe(true) 24 | expect((anthropicMessage.content as any)[0].type).toBe('text') 25 | expect((anthropicMessage.content as any)[0].text).toBe('Test message') 26 | }) 27 | 28 | it('should preserve file references when converting', () => { 29 | const openAIMessage: OpenAIMessage = { 30 | role: 'user', 31 | content: 'Analyze this file', 32 | files: [{ path: 'test.ts', content: 'const a = 1;' }] 33 | } 34 | 35 | const anthropicMessage = convertOpenAIToAnthropicMessage(openAIMessage) 36 | 37 | expect(anthropicMessage.files).toBeDefined() 38 | expect(anthropicMessage.files?.[0].path).toBe('test.ts') 39 | expect(anthropicMessage.files?.[0].content).toBe('const a = 1;') 40 | }) 41 | }) 42 | 43 | describe('convertAnthropicToOpenAIMessage', () => { 44 | it('should convert Anthropic message to OpenAI format', () => { 45 | const anthropicMessage: AnthropicMessage = { 46 | role: 'user', 47 | content: [ 48 | { type: 'text', text: 'Text part 1' }, 49 | { type: 'text', text: 'Text part 2' } 50 | ] 51 | } 52 | 53 | const openAIMessage = convertAnthropicToOpenAIMessage(anthropicMessage) 54 | 55 | expect(openAIMessage.role).toBe('user') 56 | expect(typeof openAIMessage.content).toBe('string') 57 | expect(openAIMessage.content).toBe('Text part 1\nText part 2') 58 | }) 59 | 60 | it('should handle string content in Anthropic format', () => { 61 | const anthropicMessage = { 62 | role: 'user', 63 | content: 'Already string content' 64 | } as AnthropicMessage 65 | 66 | const openAIMessage = convertAnthropicToOpenAIMessage(anthropicMessage) 67 | 68 | expect(openAIMessage.role).toBe('user') 69 | expect(openAIMessage.content).toBe('Already string content') 70 | }) 71 | }) 72 | 73 | describe('convertOpenAIToAnthropicTools', () => { 74 | it('should convert OpenAI tools to Anthropic format', () => { 75 | const openAITools: OpenAITool[] = [ 76 | { 77 | type: 'function', 78 | function: { 79 | name: 'get_weather', 80 | description: 'Get the weather for a location', 81 | parameters: { 82 | type: 'object', 83 | properties: { 84 | location: { type: 'string' } 85 | } 86 | } 87 | } 88 | } 89 | ] 90 | 91 | const anthropicTools = convertOpenAIToAnthropicTools(openAITools) 92 | 93 | expect(anthropicTools).toHaveLength(1) 94 | expect(anthropicTools[0].name).toBe('get_weather') 95 | expect(anthropicTools[0].description).toBe('Get the weather for a location') 96 | expect(anthropicTools[0].input_schema).toEqual({ 97 | type: 'object', 98 | properties: { 99 | location: { type: 'string' } 100 | } 101 | }) 102 | }) 103 | }) 104 | 105 | describe('convertAnthropicToOpenAITools', () => { 106 | it('should convert Anthropic tools to OpenAI format', () => { 107 | const anthropicTools: AnthropicTool[] = [ 108 | { 109 | name: 'get_weather', 110 | description: 'Get the weather for a location', 111 | input_schema: { 112 | type: 'object', 113 | properties: { 114 | location: { type: 'string' } 115 | } 116 | } 117 | } 118 | ] 119 | 120 | const openAITools = convertAnthropicToOpenAITools(anthropicTools) 121 | 122 | expect(openAITools).toHaveLength(1) 123 | expect(openAITools[0].type).toBe('function') 124 | expect(openAITools[0].function.name).toBe('get_weather') 125 | expect(openAITools[0].function.description).toBe('Get the weather for a location') 126 | expect(openAITools[0].function.parameters).toEqual({ 127 | type: 'object', 128 | properties: { 129 | location: { type: 'string' } 130 | } 131 | }) 132 | }) 133 | }) 134 | 135 | describe('convertMessagesToPrompt', () => { 136 | it('should convert OpenAI messages to prompt format', () => { 137 | const messages: OpenAIMessage[] = [ 138 | { role: 'system', content: 'You are a coding assistant.' }, 139 | { role: 'user', content: 'Write a hello world function.' } 140 | ] 141 | 142 | const prompt = convertMessagesToPrompt(messages) 143 | 144 | expect(prompt).toBe('SYSTEM: You are a coding assistant.\n\nUSER: Write a hello world function.') 145 | }) 146 | 147 | it('should convert Anthropic messages to prompt format', () => { 148 | const messages: AnthropicMessage[] = [ 149 | { 150 | role: 'system', 151 | content: [{ type: 'text', text: 'You are a coding assistant.' }] 152 | }, 153 | { 154 | role: 'user', 155 | content: [{ type: 'text', text: 'Write a hello world function.' }] 156 | } 157 | ] 158 | 159 | const prompt = convertMessagesToPrompt(messages) 160 | 161 | expect(prompt).toBe('SYSTEM: You are a coding assistant.\n\nUSER: Write a hello world function.') 162 | }) 163 | 164 | it('should handle mixed message formats', () => { 165 | const messages = [ 166 | { role: 'system', content: 'You are a coding assistant.' }, 167 | { 168 | role: 'user', 169 | content: [{ type: 'text', text: 'Write a hello world function.' }] 170 | } 171 | ] as Array 172 | 173 | const prompt = convertMessagesToPrompt(messages) 174 | 175 | expect(prompt).toBe('SYSTEM: You are a coding assistant.\n\nUSER: Write a hello world function.') 176 | }) 177 | }) 178 | 179 | describe('parseCliOutput', () => { 180 | it('should parse valid JSON output', () => { 181 | const output = '{"id": "test-id", "choices": [{"message": {"role": "assistant", "content": "Hello!"}}]}' 182 | 183 | const parsed = parseCliOutput(output) 184 | 185 | expect(parsed).toEqual({ 186 | id: 'test-id', 187 | choices: [{ message: { role: 'assistant', content: 'Hello!' } }] 188 | }) 189 | }) 190 | 191 | it('should handle non-JSON output as text response', () => { 192 | const output = 'Plain text response' 193 | 194 | const parsed = parseCliOutput(output) 195 | 196 | expect(parsed).toEqual({ 197 | choices: [ 198 | { 199 | message: { 200 | role: 'assistant', 201 | content: 'Plain text response' 202 | } 203 | } 204 | ] 205 | }) 206 | }) 207 | }) 208 | }) -------------------------------------------------------------------------------- /typescript/src/tests/sessions.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from 'vitest' 2 | import { Session, Sessions } from '../client/sessions' 3 | import { BaseClient } from '../client/base' 4 | 5 | // Mock the BaseClient class 6 | vi.mock('../client/base', () => { 7 | return { 8 | BaseClient: vi.fn().mockImplementation(() => { 9 | return { 10 | executeCommand: vi.fn().mockImplementation((params) => { 11 | if (params.resume) { 12 | return Promise.resolve('{"id": "resumed-session", "choices": [{"message": {"role": "assistant", "content": "Resumed session"}}]}') 13 | } else { 14 | return Promise.resolve('{"id": "new-session", "choices": [{"message": {"role": "assistant", "content": "New session"}}]}') 15 | } 16 | }) 17 | } 18 | }) 19 | } 20 | }) 21 | 22 | describe('Sessions', () => { 23 | let mockClient: BaseClient 24 | let sessions: Sessions 25 | 26 | beforeEach(() => { 27 | mockClient = new BaseClient() 28 | sessions = new Sessions(mockClient) 29 | }) 30 | 31 | describe('create', () => { 32 | it('should create a new session with messages', async () => { 33 | const session = await sessions.create({ 34 | messages: [{ role: 'user', content: 'Start session' }] 35 | }) 36 | 37 | expect(session).toBeInstanceOf(Session) 38 | expect(session.id).toBeDefined() 39 | expect(mockClient.executeCommand).toHaveBeenCalled() 40 | 41 | const params = (mockClient.executeCommand as any).mock.calls[0][0] 42 | expect(params.prompt).toBeDefined() 43 | expect(params.outputFormat).toBe('json') 44 | }) 45 | 46 | it('should create a session with a generated ID if not present in response', async () => { 47 | // Override mock to return a response without ID 48 | (mockClient.executeCommand as any).mockImplementationOnce(() => { 49 | return Promise.resolve('{"choices": [{"message": {"role": "assistant", "content": "No ID"}}]}') 50 | }) 51 | 52 | const session = await sessions.create({ 53 | messages: [{ role: 'user', content: 'Start session' }] 54 | }) 55 | 56 | expect(session).toBeInstanceOf(Session) 57 | expect(session.id).toBeDefined() 58 | expect(session.id).toContain('session_') 59 | }) 60 | }) 61 | 62 | describe('resume', () => { 63 | it('should resume an existing session by ID', async () => { 64 | const sessionId = 'test-session-id' 65 | const session = await sessions.resume(sessionId) 66 | 67 | expect(session).toBeInstanceOf(Session) 68 | expect(session.id).toBe(sessionId) 69 | expect(mockClient.executeCommand).toHaveBeenCalled() 70 | 71 | const params = (mockClient.executeCommand as any).mock.calls[0][0] 72 | expect(params.resume).toBe(sessionId) 73 | expect(params.outputFormat).toBe('json') 74 | }) 75 | }) 76 | }) 77 | 78 | describe('Session', () => { 79 | let mockClient: BaseClient 80 | let session: Session 81 | const sessionId = 'test-session-id' 82 | const initialMessages = [{ role: 'user' as const, content: 'Initial message' }] 83 | 84 | beforeEach(() => { 85 | mockClient = new BaseClient() 86 | session = new Session(sessionId, mockClient, initialMessages) 87 | }) 88 | 89 | describe('continue', () => { 90 | it('should continue a session with new messages', async () => { 91 | const response = await session.continue({ 92 | messages: [{ role: 'user', content: 'Continue session' }] 93 | }) 94 | 95 | expect(response).toBeDefined() 96 | expect(mockClient.executeCommand).toHaveBeenCalled() 97 | 98 | const params = (mockClient.executeCommand as any).mock.calls[0][0] 99 | expect(params.prompt).toBeDefined() 100 | expect(params.resume).toBe(sessionId) 101 | expect(params.outputFormat).toBe('json') 102 | }) 103 | }) 104 | 105 | describe('getMessages', () => { 106 | it('should return all messages in the session', () => { 107 | const messages = session.getMessages() 108 | 109 | expect(messages).toEqual(initialMessages) 110 | 111 | // Continue the session to add more messages 112 | session.continue({ 113 | messages: [{ role: 'user', content: 'Continue session' }] 114 | }) 115 | 116 | const updatedMessages = session.getMessages() 117 | expect(updatedMessages).toHaveLength(2) 118 | expect(updatedMessages[0]).toEqual(initialMessages[0]) 119 | expect(updatedMessages[1]).toEqual({ role: 'user', content: 'Continue session' }) 120 | }) 121 | 122 | it('should return a copy of the messages array', () => { 123 | const messages = session.getMessages() 124 | 125 | // Modify the returned array 126 | messages.push({ role: 'user', content: 'New message' }) 127 | 128 | // Original messages in the session should not be affected 129 | const internalMessages = session.getMessages() 130 | expect(internalMessages).toHaveLength(1) 131 | expect(internalMessages[0]).toEqual(initialMessages[0]) 132 | }) 133 | }) 134 | }) -------------------------------------------------------------------------------- /typescript/src/types/index.ts: -------------------------------------------------------------------------------- 1 | // Common types used across both OpenAI and Anthropic style APIs 2 | 3 | export type Role = 'user' | 'assistant' | 'system' 4 | 5 | // OpenAI Style Types 6 | export interface OpenAIMessage { 7 | role: Role 8 | content: string 9 | files?: Array 10 | } 11 | 12 | export interface OpenAIFunction { 13 | name: string 14 | description?: string 15 | parameters?: Record 16 | } 17 | 18 | export interface OpenAITool { 19 | type: 'function' 20 | function: OpenAIFunction 21 | } 22 | 23 | export interface OpenAICompletionChoice { 24 | index: number 25 | message: { 26 | role: Role 27 | content: string 28 | } 29 | finish_reason: string 30 | } 31 | 32 | export interface OpenAIStreamChoice { 33 | index: number 34 | delta: { 35 | role?: Role 36 | content?: string 37 | } 38 | finish_reason: string | null 39 | } 40 | 41 | export interface OpenAIUsage { 42 | prompt_tokens: number 43 | completion_tokens: number 44 | total_tokens: number 45 | } 46 | 47 | export interface OpenAIChatCompletion { 48 | id: string 49 | object: 'chat.completion' 50 | created: number 51 | model: string 52 | choices: Array 53 | usage: OpenAIUsage 54 | } 55 | 56 | export interface OpenAIChatCompletionChunk { 57 | id: string 58 | object: 'chat.completion.chunk' 59 | created: number 60 | model: string 61 | choices: Array 62 | } 63 | 64 | export interface OpenAIChatCompletionCreateParams { 65 | model: string 66 | messages: Array 67 | temperature?: number 68 | max_tokens?: number 69 | top_p?: number 70 | tools?: Array 71 | stream?: boolean 72 | stop?: string | Array 73 | timeout?: number 74 | } 75 | 76 | // Anthropic Style Types 77 | export interface ContentBlock { 78 | type: 'text' | 'image' 79 | text?: string 80 | source?: { 81 | type: 'base64' | 'url' 82 | media_type: string 83 | data: string 84 | } 85 | } 86 | 87 | export interface AnthropicMessage { 88 | role: Role 89 | content: string | Array 90 | files?: Array 91 | } 92 | 93 | export interface AnthropicTool { 94 | name: string 95 | description?: string 96 | input_schema: Record 97 | } 98 | 99 | export interface AnthropicUsage { 100 | input_tokens: number 101 | output_tokens: number 102 | } 103 | 104 | export interface AnthropicMessageResponse { 105 | id: string 106 | type: 'message' 107 | role: 'assistant' 108 | model: string 109 | content: Array 110 | usage: AnthropicUsage 111 | stop_reason: string 112 | } 113 | 114 | export interface AnthropicMessageStreamPart { 115 | type: 'content_block_start' | 'content_block_delta' | 'content_block_stop' | 'message_stop' 116 | index?: number 117 | delta?: { 118 | type?: string 119 | text?: string 120 | } 121 | } 122 | 123 | export interface AnthropicMessageCreateParams { 124 | model: string 125 | messages: Array 126 | max_tokens?: number 127 | temperature?: number 128 | top_p?: number 129 | tools?: Array 130 | stream?: boolean 131 | stop_sequences?: Array 132 | timeout?: number 133 | } 134 | 135 | // Common File Reference Type 136 | export interface FileReference { 137 | path: string 138 | content?: string 139 | } 140 | 141 | // Claude Code Specific Types 142 | export interface ClaudeCodeError extends Error { 143 | status?: number 144 | code?: string 145 | param?: string 146 | } 147 | 148 | export interface SessionParams { 149 | messages: Array 150 | model?: string 151 | } 152 | 153 | export interface SessionContinueParams { 154 | messages: Array 155 | } 156 | 157 | // Common Options Type 158 | export interface ClaudeCodeOptions { 159 | apiKey?: string 160 | cliPath?: string 161 | timeout?: number 162 | } 163 | 164 | // Automation Types 165 | export interface AutomationOptions { 166 | type: 'github-review' | 'github-pr' | 'jira-ticket' 167 | config?: Record 168 | } -------------------------------------------------------------------------------- /typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "**/*.test.ts"] 15 | } --------------------------------------------------------------------------------