claude-saga


Nameclaude-saga JSON
Version 0.1.0 PyPI version JSON
download
home_pageNone
SummaryA Redux Saga-like effect system for Python, designed for Claude Code hooks
upload_time2025-08-30 14:04:40
maintainerNone
docs_urlNone
authorIain
requires_python>=3.12
licenseMIT
keywords claude effects functional generator hooks saga side-effects
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Claude Saga
A side-effect manager for Python scripts, specifically designed for building maintainable (easy to build, test, debug) [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks), inspired by [Redux Saga](https://redux-saga.js.org/).

#### Disclaimer:
Unstable - API subject to change.

## Quick Start
### Conceptual overview
```python
from claude_saga import (
    BaseSagaState, SagaRuntime,
    Call, Put, Select, Log, Stop, Complete,
    run_command_effect
)

def add(x, y):
    return x + y

class State(BaseSagaState):
    math_result: int = 2
    command_result: str = ""

def my_saga():
    yield Log("info", "Starting saga")
    initial_state = yield Select()
    math_result = yield Call(add, initial_state.math_result, 3)
    command_result = yield Call(run_command_effect, "echo 'Hello World'")
    if command_result is None:
        yield Log("error", "unable to run command")
        yield Stop("hook failed, exited early")
    yield Put({"command_result": command_result.stdout, "math_result": math_result})
    yield Complete("Saga completed successfully")

runtime = SagaRuntime(State())
final_state = runtime.run(my_saga())
print(final_state.to_json())
```

## Building Claude Code Hooks

Claude Saga handles input/output conventions of claude code hooks:

```python
#!/usr/bin/env python
import json
import sys
from claude_saga import (
    BaseSagaState, SagaRuntime,
    validate_input_saga, parse_json_saga,
    Complete
)

class HookState(BaseSagaState):
    # Add your custom state fields
    pass

def main_saga():
    # Validate and parse input
    # https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input
    yield from validate_input_saga()
    # Adds input data to state
    yield from parse_json_saga()
    
    # Your hook logic here
    
    # Complete
    yield Complete("Hook executed successfully")

def main():
    runtime = SagaRuntime(HookState())
    # Final state is an object that conforms to common json fields:
    # https://docs.anthropic.com/en/docs/claude-code/hooks#common-json-fields
    final_state = runtime.run(main_saga())
    # Claude Code exit code behavior:
    # https://docs.anthropic.com/en/docs/claude-code/hooks#simple%3A-exit-code
    print(json.dumps(final_state.to_json()))
    sys.exit(0 if final_state.continue_ else 1)

if __name__ == "__main__":
    main()
```


## Effect Types

### Call
Execute functions, including(and especially) those with side-effects:
```python
result = yield Call(function, arg1, arg2, kwarg=value)
```

### Put
Update the state:
```python
yield Put({"field": "value"})
# or with a function
yield Put(lambda state: MyState(counter=state.counter + 1))
```

### Select
Read from the state:
```python
state = yield Select()
# or with a selector
counter = yield Select(lambda state: state.counter)
```

### Log
Log messages at different levels:
```python
yield Log("info", "Information message")
yield Log("error", "Error message")
yield Log("debug", "Debug message")  # Only shown with DEBUG=1
```

### Stop
Stop execution with an error, hook output contains `continue:false`:
```python
yield Stop("Error message")
```

### Complete
Complete saga successfully, hook output contains `continue:true`:
```python
yield Complete("Success message")
```

## Common Effects

The library includes common effects 
- `log_info`, `log_error`, `log_debug`
- `run_command_effect(cmd, cwd=None, capture_output=True)` - Run shell commands
- `write_file_effect(path, content)` - Write files
- `change_directory_effect(path)` - Change working directory
- `create_directory_effect(path)` - Create directories
- `connect_pycharm_debugger_effect()` - Connect to PyCharm debugger

Notes:
  - When you write your own effects - you don't need to implement error handling - the saga runtime handles Call errors (logs them to stdout) and returns `None` on failure. 
    - If you want to terminate the saga on effect failure, check if the Call result is `None` and yield a `Stop`.

## Common Sagas

Pre-built sagas for common tasks:

- `validate_input_saga()` - Validate stdin input is provided
- `parse_json_saga()` - Parse JSON from stdin into hook state (parses specifically for [Claude Code hook input](https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input))

# Development

### Setup

```bash
uv pip install -e .
```
### Examples

The `examples/` directory contains a practical demonstration:

- `simple_command_validator.py` - Claude Code hook for validating bash commands (saga version of the [official example](https://docs.anthropic.com/en/docs/claude-code/hooks#exit-code-example%3A-bash-command-validation))

```bash
# This will fail since the expected input to stdin is not provided
uv run examples/simple_command_validator.py

# Handle claude code stdin conventions, this command passes validation 
echo '{"tool_name": "Bash", "tool_input": {"command": "ls -la"}}' | uv run examples/simple_command_validator.py

# This command fails validation (uses grep instead of rg)
echo '{"tool_name": "Bash", "tool_input": {"command": "grep pattern file.txt"}}' | uv run examples/simple_command_validator.py

```
### Running Tests

#### Unit Tests
Test the core saga framework components:
```bash
uv run pytest tests/test_claude_saga.py -v
```

#### E2E Tests  
Test complete example hook behavior:
```bash
uv run pytest tests/test_e2e_simple_command_validator.py -v
```

#### All Tests
Run the complete test suite:
```bash
uv run pytest tests/ -v
```
### Building

```bash
uv build
```

## License

MIT License - see LICENSE file for details.

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
I'd like to hear what common effects can be added to the core lib. e.g.
- http_request_effect
- mcp_request_effect

Future work must incorporate
- parsing & validation for each hook's unique input/output behaviors, fields etc...
- retry-able effects
- cancel-able effects
- parallel effects (e.g. `All`), see hypothetical async effects like `mcp_request` etc...
- concurrent effects

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "claude-saga",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.12",
    "maintainer_email": null,
    "keywords": "claude, effects, functional, generator, hooks, saga, side-effects",
    "author": "Iain",
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/6e/1e/7db10ea88d646cd0e39a5fb8c502b323c0fc80302affae9d62dfd3fb69dd/claude_saga-0.1.0.tar.gz",
    "platform": null,
    "description": "# Claude Saga\nA side-effect manager for Python scripts, specifically designed for building maintainable (easy to build, test, debug) [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks), inspired by [Redux Saga](https://redux-saga.js.org/).\n\n#### Disclaimer:\nUnstable - API subject to change.\n\n## Quick Start\n### Conceptual overview\n```python\nfrom claude_saga import (\n    BaseSagaState, SagaRuntime,\n    Call, Put, Select, Log, Stop, Complete,\n    run_command_effect\n)\n\ndef add(x, y):\n    return x + y\n\nclass State(BaseSagaState):\n    math_result: int = 2\n    command_result: str = \"\"\n\ndef my_saga():\n    yield Log(\"info\", \"Starting saga\")\n    initial_state = yield Select()\n    math_result = yield Call(add, initial_state.math_result, 3)\n    command_result = yield Call(run_command_effect, \"echo 'Hello World'\")\n    if command_result is None:\n        yield Log(\"error\", \"unable to run command\")\n        yield Stop(\"hook failed, exited early\")\n    yield Put({\"command_result\": command_result.stdout, \"math_result\": math_result})\n    yield Complete(\"Saga completed successfully\")\n\nruntime = SagaRuntime(State())\nfinal_state = runtime.run(my_saga())\nprint(final_state.to_json())\n```\n\n## Building Claude Code Hooks\n\nClaude Saga handles input/output conventions of claude code hooks:\n\n```python\n#!/usr/bin/env python\nimport json\nimport sys\nfrom claude_saga import (\n    BaseSagaState, SagaRuntime,\n    validate_input_saga, parse_json_saga,\n    Complete\n)\n\nclass HookState(BaseSagaState):\n    # Add your custom state fields\n    pass\n\ndef main_saga():\n    # Validate and parse input\n    # https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input\n    yield from validate_input_saga()\n    # Adds input data to state\n    yield from parse_json_saga()\n    \n    # Your hook logic here\n    \n    # Complete\n    yield Complete(\"Hook executed successfully\")\n\ndef main():\n    runtime = SagaRuntime(HookState())\n    # Final state is an object that conforms to common json fields:\n    # https://docs.anthropic.com/en/docs/claude-code/hooks#common-json-fields\n    final_state = runtime.run(main_saga())\n    # Claude Code exit code behavior:\n    # https://docs.anthropic.com/en/docs/claude-code/hooks#simple%3A-exit-code\n    print(json.dumps(final_state.to_json()))\n    sys.exit(0 if final_state.continue_ else 1)\n\nif __name__ == \"__main__\":\n    main()\n```\n\n\n## Effect Types\n\n### Call\nExecute functions, including(and especially) those with side-effects:\n```python\nresult = yield Call(function, arg1, arg2, kwarg=value)\n```\n\n### Put\nUpdate the state:\n```python\nyield Put({\"field\": \"value\"})\n# or with a function\nyield Put(lambda state: MyState(counter=state.counter + 1))\n```\n\n### Select\nRead from the state:\n```python\nstate = yield Select()\n# or with a selector\ncounter = yield Select(lambda state: state.counter)\n```\n\n### Log\nLog messages at different levels:\n```python\nyield Log(\"info\", \"Information message\")\nyield Log(\"error\", \"Error message\")\nyield Log(\"debug\", \"Debug message\")  # Only shown with DEBUG=1\n```\n\n### Stop\nStop execution with an error, hook output contains `continue:false`:\n```python\nyield Stop(\"Error message\")\n```\n\n### Complete\nComplete saga successfully, hook output contains `continue:true`:\n```python\nyield Complete(\"Success message\")\n```\n\n## Common Effects\n\nThe library includes common effects \n- `log_info`, `log_error`, `log_debug`\n- `run_command_effect(cmd, cwd=None, capture_output=True)` - Run shell commands\n- `write_file_effect(path, content)` - Write files\n- `change_directory_effect(path)` - Change working directory\n- `create_directory_effect(path)` - Create directories\n- `connect_pycharm_debugger_effect()` - Connect to PyCharm debugger\n\nNotes:\n  - When you write your own effects - you don't need to implement error handling - the saga runtime handles Call errors (logs them to stdout) and returns `None` on failure. \n    - If you want to terminate the saga on effect failure, check if the Call result is `None` and yield a `Stop`.\n\n## Common Sagas\n\nPre-built sagas for common tasks:\n\n- `validate_input_saga()` - Validate stdin input is provided\n- `parse_json_saga()` - Parse JSON from stdin into hook state (parses specifically for [Claude Code hook input](https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input))\n\n# Development\n\n### Setup\n\n```bash\nuv pip install -e .\n```\n### Examples\n\nThe `examples/` directory contains a practical demonstration:\n\n- `simple_command_validator.py` - Claude Code hook for validating bash commands (saga version of the [official example](https://docs.anthropic.com/en/docs/claude-code/hooks#exit-code-example%3A-bash-command-validation))\n\n```bash\n# This will fail since the expected input to stdin is not provided\nuv run examples/simple_command_validator.py\n\n# Handle claude code stdin conventions, this command passes validation \necho '{\"tool_name\": \"Bash\", \"tool_input\": {\"command\": \"ls -la\"}}' | uv run examples/simple_command_validator.py\n\n# This command fails validation (uses grep instead of rg)\necho '{\"tool_name\": \"Bash\", \"tool_input\": {\"command\": \"grep pattern file.txt\"}}' | uv run examples/simple_command_validator.py\n\n```\n### Running Tests\n\n#### Unit Tests\nTest the core saga framework components:\n```bash\nuv run pytest tests/test_claude_saga.py -v\n```\n\n#### E2E Tests  \nTest complete example hook behavior:\n```bash\nuv run pytest tests/test_e2e_simple_command_validator.py -v\n```\n\n#### All Tests\nRun the complete test suite:\n```bash\nuv run pytest tests/ -v\n```\n### Building\n\n```bash\nuv build\n```\n\n## License\n\nMIT License - see LICENSE file for details.\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\nI'd like to hear what common effects can be added to the core lib. e.g.\n- http_request_effect\n- mcp_request_effect\n\nFuture work must incorporate\n- parsing & validation for each hook's unique input/output behaviors, fields etc...\n- retry-able effects\n- cancel-able effects\n- parallel effects (e.g. `All`), see hypothetical async effects like `mcp_request` etc...\n- concurrent effects\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "A Redux Saga-like effect system for Python, designed for Claude Code hooks",
    "version": "0.1.0",
    "project_urls": {
        "Homepage": "https://github.com/yourusername/claude-saga",
        "Issues": "https://github.com/yourusername/claude-saga/issues",
        "Repository": "https://github.com/yourusername/claude-saga"
    },
    "split_keywords": [
        "claude",
        " effects",
        " functional",
        " generator",
        " hooks",
        " saga",
        " side-effects"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "dee6b45f182dbfcf91e7f7587744ba62bdc6a89a81b034456f467bd54d42c77d",
                "md5": "732c05bf9b393a1bae59d9c6c184ab5a",
                "sha256": "32cc46d9f8667c65975a7503419733e1895f1a11ee8897b33bb920cadb8b6114"
            },
            "downloads": -1,
            "filename": "claude_saga-0.1.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "732c05bf9b393a1bae59d9c6c184ab5a",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.12",
            "size": 7729,
            "upload_time": "2025-08-30T14:04:38",
            "upload_time_iso_8601": "2025-08-30T14:04:38.737654Z",
            "url": "https://files.pythonhosted.org/packages/de/e6/b45f182dbfcf91e7f7587744ba62bdc6a89a81b034456f467bd54d42c77d/claude_saga-0.1.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "6e1e7db10ea88d646cd0e39a5fb8c502b323c0fc80302affae9d62dfd3fb69dd",
                "md5": "e2859ee97d1aeb1ff056db5139715a92",
                "sha256": "e4844ce0242d3a349beb0f50615874a7079fe87b3912fdb2f62f5b18202b820a"
            },
            "downloads": -1,
            "filename": "claude_saga-0.1.0.tar.gz",
            "has_sig": false,
            "md5_digest": "e2859ee97d1aeb1ff056db5139715a92",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.12",
            "size": 10935,
            "upload_time": "2025-08-30T14:04:40",
            "upload_time_iso_8601": "2025-08-30T14:04:40.073235Z",
            "url": "https://files.pythonhosted.org/packages/6e/1e/7db10ea88d646cd0e39a5fb8c502b323c0fc80302affae9d62dfd3fb69dd/claude_saga-0.1.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-30 14:04:40",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "yourusername",
    "github_project": "claude-saga",
    "github_not_found": true,
    "lcname": "claude-saga"
}
        
Elapsed time: 0.96002s