Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions CLAUDE.md → .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ Python SDK for programmatically pulling and using Musher bundle assets.

```
src/musher/
├── _types.py # Enums, BundleRef, OCI constants (no deps)
├── _types.py # Enums, BundleRef (no deps)
├── _errors.py # Exception hierarchy (no deps)
├── _paths.py # Platform-aware directory resolution (deps: platformdirs)
├── _config.py # Global configuration (deps: _paths)
├── _auth.py # Credential resolution chain (deps: _paths)
├── _bundle.py # Pydantic models for bundles/manifests (deps: _types)
├── _cache.py # XDG-compliant disk cache (deps: _types, _errors)
├── _oci.py # Low-level OCI interaction (deps: _types, _errors)
├── _cache.py # Content-addressable disk cache (deps: _types, _errors)
├── _cache_info.py # Cache inspection types (no deps)
├── _http.py # HTTP transport with error mapping (deps: _types, _errors)
├── _handles.py # Typed resource handles (deps: _types, _export)
├── _export.py # Framework export dataclasses (no deps)
├── _client.py # Client + AsyncClient (deps: all above)
├── adapters/ # Future framework adapters (LangChain, LlamaIndex)
└── __init__.py # Public API re-exports
```

Expand All @@ -27,6 +30,7 @@ task check:lint # Ruff linter
task check:types # basedpyright type checker
task check:test # pytest
task check:format # Format with ruff
task check:fix # Auto-fix lint issues and format
```

## Code Standards
Expand All @@ -35,4 +39,4 @@ task check:format # Format with ruff
- Pydantic models use `alias_generator=to_camel` for camelCase wire format
- All public API is re-exported from `__init__.py`
- Internal modules are prefixed with `_` (private)
- Stub methods raise `NotImplementedError` until implemented
- `BundleCache` is internal — not exported in `__all__`
32 changes: 32 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Bug Report
description: Report a bug in the Musher Python SDK
labels: ["bug"]
body:
- type: textarea
id: description
attributes:
label: Description
description: A clear description of the bug.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: Minimal code or steps to reproduce the issue.
validations:
required: true
- type: input
id: version
attributes:
label: SDK version
description: "Output of: python -c \"import musher; print(musher.__version__)\""
validations:
required: true
- type: input
id: python-version
attributes:
label: Python version
description: "Output of: python --version"
validations:
required: true
18 changes: 18 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Feature Request
description: Suggest a feature for the Musher Python SDK
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: Problem
description: What problem does this solve?
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed solution
description: How would you like this to work?
validations:
required: true
7 changes: 7 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Summary

<!-- What does this PR do? -->

## Test plan

<!-- How was this tested? -->
File renamed without changes.
19 changes: 19 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
github-actions:
patterns:
- "*"

- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
groups:
python-deps:
patterns:
- "*"
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ permissions:
jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.13"]
steps:
- uses: actions/checkout@v6

Expand All @@ -25,7 +29,7 @@ jobs:

- uses: actions/setup-python@v6
with:
python-version: "3.13"
python-version: ${{ matrix.python-version }}

- run: task setup

Expand Down
156 changes: 123 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,184 @@
<div align="center">

# Musher Python SDK

[![CI](https://github.com/musher-dev/python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/musher-dev/python-sdk/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/musher-sdk)](https://pypi.org/project/musher-sdk/)

Python SDK for the [Musher](https://musher.dev) bundle distribution platform. Pull versioned AI agent asset bundles (prompts, configs, tool definitions) into your Python applications.
Python SDK for the [Musher](https://musher.dev) bundle distribution platform. Pull versioned AI agent asset bundles — prompts, tool definitions, agent specs, and skills — into your Python applications.

</div>

## Installation

Requires **Python 3.13+**.

```bash
pip install musher-sdk
```

## Authentication

Set your API key as an environment variable:

```bash
export MUSHER_API_KEY="msk_..."
```

The SDK resolves credentials automatically in this order:

1. `MUSHER_API_KEY` environment variable
2. OS keyring (`musher/{hostname}`)
3. Credential file (`<data_dir>/credentials/<host_id>/api-key`, must be `0600`)

You can also pass a token directly:

```python
musher.configure(token="msk_...")
```

## Quick Start

```python
import musher

# Explicit token
musher.configure(token="your-token")

bundle = musher.pull("myorg/my-bundle:1.0.0")

for asset in bundle.files():
print(f"{asset.logical_path}: {len(asset.text())} chars")
for f in bundle.files():
print(f"{f.logical_path}: {len(f.text())} chars")
```

### Async

```python
import musher

async with musher.AsyncClient() as client:
bundle = await client.pull("myorg/my-bundle:1.0.0")
```

### Sync
### Sync Client

```python
import musher

with musher.Client() as client:
bundle = client.pull("myorg/my-bundle:1.0.0")

result = client.resolve("myorg/my-bundle:1.0.0")
asset = client.fetch_asset("asset-id", version="1.0.0")

asset = client.fetch_asset(
"prompts/system.md",
namespace="myorg",
slug="my-bundle",
version="1.0.0",
)
```

## Configuration
## Working with Bundles

### Credential Chain
Bundles provide typed accessors for each resource type:

The SDK resolves credentials automatically in this order:
```python
bundle = musher.pull("myorg/my-bundle:1.0.0")

1. **Environment variables** — `MUSHER_API_KEY`
2. **OS keyring** — host-scoped service `musher/{hostname}`
3. **File fallback** — `<data_dir>/credentials/<host_id>/api-key` (must be `0600` permissions)
# Prompts
prompt = bundle.prompt("system")
print(prompt.text())

# Skills
for skill in bundle.skills():
print(skill.name, skill.description)

# Toolsets and agent specs
toolset = bundle.toolset("search-tools")
data = toolset.parse_json()

spec = bundle.agent_spec("reviewer")
config = spec.parse_json()
```

Filter to a subset of resources with `select()`:

```python
selection = bundle.select(skills=["code-review"], prompts=["system"])
```

## Framework Integrations

The SDK integrates with popular AI agent frameworks:

- **Claude** — export bundles as Claude plugins or install skills to `.claude/skills/` ([examples](examples/claude/))
- **OpenAI Agents** — export skills as local directories or inline zips for the OpenAI shell tool ([examples](examples/openai/))
- **PydanticAI** — use bundle prompts as agent instructions with structured output ([examples](examples/pydantic_ai/))

## Configuration

### Registry URL

The registry URL is resolved from environment variables:
```bash
export MUSHER_API_URL="https://custom-registry.example.com"
```

- `MUSHER_API_URL`
- Default: `https://api.musher.dev`
Default: `https://api.musher.dev`

### Programmatic Configuration

```python
from pathlib import Path
import musher

# All parameters are optional — omitted values auto-discover
musher.configure(
token="your-token",
registry_url="https://custom.dev",
cache_dir=Path("/tmp/cache"),
token="msk_...",
registry_url="https://custom-registry.example.com",
cache_dir=Path("/tmp/musher-cache"),
verify_checksums=True,
timeout=30.0,
max_retries=2,
)
```

### Cache Behavior
All parameters are optional — omitted values are auto-discovered.

See [Configuration](docs/configuration.md) for the full reference (directory layout, credential chain, cache structure, TTL defaults).

### Cache

The SDK uses a content-addressable disk cache:

- Blobs are stored by SHA-256 hash (shared across registries)
- Manifests and refs are partitioned by registry hostname
- Manifests have a configurable TTL (default 24h); refs default to 5min
- `clean()` removes expired entries and garbage-collects unreferenced blobs
- Blobs stored by SHA-256 hash, shared across registries
- Manifests and refs partitioned by registry hostname
- Manifests TTL: 24h; refs TTL: 5min
- `cache_clean()` removes expired entries and garbage-collects unreferenced blobs

Cache management functions are available at the module level and on the `Client`:

```python
import musher

info = musher.cache_info() # cache statistics
musher.cache_remove("myorg/my-bundle:1.0.0") # remove a specific bundle
musher.cache_clean() # remove expired entries
musher.cache_clear() # remove all cached data
path = musher.cache_path() # cache directory path
```

## What's Implemented
## Features

- `resolve()` — resolve bundle references to manifests
- `fetch_asset()` — fetch individual assets by ID
- `pull()` — resolve + fetch all assets + verify checksums
- `pull()` / `pull_async()` — resolve + fetch all assets + verify checksums
- `resolve()` / `resolve_async()` — resolve bundle references to manifests
- `fetch_asset()` — fetch individual assets by logical path
- Sync (`Client`) and async (`AsyncClient`) clients
- Typed handles: `SkillHandle`, `PromptHandle`, `ToolsetHandle`, `AgentSpecHandle`
- `bundle.select()` — filter resources by type
- Content-addressable cache with TTL and garbage collection
- Typed handles: skills, prompts, toolsets, agent specs
- `export_claude_plugin()` — export bundle as a Claude plugin
- `install_claude_skills()` — install skills to a directory
- Cache management: `cache_info()`, `cache_remove()`, `cache_clear()`, `cache_clean()`, `cache_path()`
- `export_claude_plugin()` / `install_claude_skills()` — Claude integration
- `export_openai_local_skill()` / `export_openai_inline_skill()` — OpenAI Agents integration

## Examples

See the [examples/](examples/) directory for runnable code samples covering basic usage, Claude, OpenAI Agents, and PydanticAI integrations.

## License

Expand Down
6 changes: 6 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ tasks:
- task: check:types
- task: check:test

check:fix:
desc: Auto-fix lint issues and format
cmds:
- uv run ruff check --fix .
- uv run ruff format .

check:format:
desc: Format code with ruff
cmds:
Expand Down
Loading
Loading