diff --git a/CLAUDE.md b/.claude/CLAUDE.md
similarity index 63%
rename from CLAUDE.md
rename to .claude/CLAUDE.md
index c32ea36..9cbbd2b 100644
--- a/CLAUDE.md
+++ b/.claude/CLAUDE.md
@@ -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
```
@@ -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
@@ -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__`
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..4bb8648
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -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
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..cb8739f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -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
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..4b0487c
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,7 @@
+## Summary
+
+
+
+## Test plan
+
+
diff --git a/SECURITY.md b/.github/SECURITY.md
similarity index 100%
rename from SECURITY.md
rename to .github/SECURITY.md
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..88c1dee
--- /dev/null
+++ b/.github/dependabot.yml
@@ -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:
+ - "*"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3be344d..b20b8cd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
@@ -25,7 +29,7 @@ jobs:
- uses: actions/setup-python@v6
with:
- python-version: "3.13"
+ python-version: ${{ matrix.python-version }}
- run: task setup
diff --git a/README.md b/README.md
index 1e950c5..5ccddbd 100644
--- a/README.md
+++ b/README.md
@@ -1,94 +1,184 @@
+
+
# Musher Python SDK
[](https://github.com/musher-dev/python-sdk/actions/workflows/ci.yml)
[](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.
+
+
## 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 (`/credentials//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** — `/credentials//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
diff --git a/Taskfile.yml b/Taskfile.yml
index 7b26310..3e74588 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -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:
diff --git a/CONFIGURATION.md b/docs/configuration.md
similarity index 81%
rename from CONFIGURATION.md
rename to docs/configuration.md
index c5a7049..d9d9622 100644
--- a/CONFIGURATION.md
+++ b/docs/configuration.md
@@ -32,8 +32,7 @@ On Windows, the SDK uses a flat layout under `%LOCALAPPDATA%\musher\` with categ
| Variable | Purpose |
|---|---|
-| `MUSHER_API_URL` | Registry URL (checked first) |
-| `MUSHER_BASE_URL` | Registry URL alias |
+| `MUSHER_API_URL` | Registry URL |
Default: `https://api.musher.dev`
@@ -46,22 +45,9 @@ The SDK resolves credentials in this order, stopping at the first match:
1. **Environment variables** — `MUSHER_API_KEY`
2. **OS keyring** — service `musher/{hostname}`, username `api-key`
- Hostname is derived from the registry URL (e.g. `musher/api.musher.dev`)
-3. **Profile config file** — `/config.toml`
- - Format: `[profile.] api_key = "..."`
- - Default profile: `"default"`
-4. **File fallback** — `/api-key`
+3. **File fallback** — `/credentials//api-key`
- Must have `0600` permissions (owner-only); rejected otherwise
-### Profile Config Format
-
-```toml
-[profile.default]
-api_key = "mush_prod_..."
-
-[profile.staging]
-api_key = "mush_staging_..."
-```
-
## Cache Structure
```
@@ -93,14 +79,13 @@ $cache_root/
```python
import musher
+from pathlib import Path
musher.configure(
token="...", # explicit token
- api_key="...", # alias for token
registry_url="https://...", # explicit registry URL
- api_url="https://...", # alias for registry_url
cache_dir=Path("..."), # override cache directory
)
```
-When neither `token` nor `api_key` is provided, the credential chain is used automatically. When neither `registry_url` nor `api_url` is provided, the URL env var chain is checked.
+When `token` is not provided, the credential chain is used automatically. When `registry_url` is not provided, the URL env var chain is checked.
diff --git a/examples/README.md b/examples/README.md
index efa6b9f..503c142 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,5 +1,7 @@
# Musher SDK Examples
+> Part of the [Musher Python SDK](../README.md).
+
Examples for the Musher Python SDK, organized by use case.
## Prerequisites
@@ -38,10 +40,18 @@ Credentials are auto-discovered in this order:
To override explicitly in code: `musher.configure(token="your-token")`.
-## Running
+## Bundles
+
+Examples use published bundles from [hub.musher.dev](https://hub.musher.dev/):
-Replace placeholder bundle references (e.g. `"acme/prompt-library:1.2.0"`) with
-real ones from your Musher registry, then:
+| Bundle | Version | Used by |
+|--------|---------|---------|
+| `musher-examples/agent-toolkit` | 2.0.0 | `basics/pull_bundle.py`, `basics/inspect_manifest.py` |
+| `musher-examples/prompt-library` | 1.2.0 | `basics/bundle_resources.py`, `pydantic_ai/*` |
+| `musher-examples/code-review-kit` | 1.2.0 | `claude/*`, `openai/local_shell_skill.py` |
+| `musher-examples/data-workflows` | 1.0.0 | `openai/hosted_inline_skill.py` |
+
+## Running
```bash
uv run python examples/openai/hosted_inline_skill.py
@@ -61,7 +71,3 @@ uv run python examples/openai/hosted_inline_skill.py
| `pydantic_ai/instructions_from_bundle.py` | Use bundle prompts as PydanticAI agent instructions | `pydantic-ai` | Working |
| `pydantic_ai/structured_output.py` | Structured output with bundle-managed prompts | `pydantic-ai` | Working |
| `pydantic_ai/dynamic_instructions.py` | Dynamic instructions and tools from bundles | `pydantic-ai` | Working |
-
-All examples use implemented SDK methods. Bundle references (e.g.
-`"acme/prompt-library:1.2.0"`) are placeholders — replace them with a real
-bundle ref from your Musher registry before running.
diff --git a/examples/basics/bundle_resources.py b/examples/basics/bundle_resources.py
index d9eadcf..0778ac1 100644
--- a/examples/basics/bundle_resources.py
+++ b/examples/basics/bundle_resources.py
@@ -2,13 +2,10 @@
import musher
-# NOTE: Bundle references below (e.g. "acme/prompt-library:1.2.0") are
-# placeholders. Replace with a real bundle ref from your Musher registry.
-
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
# or credential file. To override: musher.configure(token="your-token")
-bundle = musher.pull("acme/prompt-library:1.2.0")
+bundle = musher.pull("musher-examples/prompt-library:1.2.0")
# Access versioned prompts by name
system_prompt = bundle.prompt("system")
diff --git a/examples/basics/inspect_manifest.py b/examples/basics/inspect_manifest.py
index 2a0c549..7de6f6a 100644
--- a/examples/basics/inspect_manifest.py
+++ b/examples/basics/inspect_manifest.py
@@ -2,10 +2,7 @@
import musher
-# NOTE: Bundle references below (e.g. "acme/agent-toolkit:2.0.0") are
-# placeholders. Replace with a real bundle ref from your Musher registry.
-
-result = musher.resolve("acme/agent-toolkit:2.0.0")
+result = musher.resolve("musher-examples/agent-toolkit:2.0.0")
print(f"Bundle: {result.ref} v{result.version}")
print(f"State: {result.state}")
diff --git a/examples/basics/pull_bundle.py b/examples/basics/pull_bundle.py
index 2499943..7b16ef5 100644
--- a/examples/basics/pull_bundle.py
+++ b/examples/basics/pull_bundle.py
@@ -2,22 +2,19 @@
import musher
-# NOTE: Bundle references below (e.g. "acme/agent-toolkit:2.0.0") are
-# placeholders. Replace with a real bundle ref from your Musher registry.
-
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
# or credential file. To override: musher.configure(token="your-token")
-bundle = musher.pull("acme/agent-toolkit:2.0.0")
+bundle = musher.pull("musher-examples/agent-toolkit:2.0.0")
# List all files
for fh in bundle.files():
print(f"{fh.logical_path} ({fh.media_type or 'unknown'})")
# Get a specific file
-prompt = bundle.file("prompts/main.txt")
+prompt = bundle.file("prompts/system.md")
if prompt:
- print(f"Prompt content: {prompt.text()}")
+ print(f"System prompt: {prompt.text()[:120]}...")
# For reproducible deployments, pass a digest ref instead of a version tag:
-# "acme/agent-toolkit@sha256:abc123def456"
+# "musher-examples/agent-toolkit@sha256:abc123def456"
diff --git a/examples/claude/export_plugin.py b/examples/claude/export_plugin.py
index b4f2a9a..6b0e23e 100644
--- a/examples/claude/export_plugin.py
+++ b/examples/claude/export_plugin.py
@@ -13,20 +13,17 @@
import musher
-# NOTE: Bundle references below (e.g. "acme/engineering-workflows:2.0.0") are
-# placeholders. Replace with a real bundle ref from your Musher registry.
-
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
# or credential file. To override: musher.configure(token="your-token")
-bundle = musher.pull("acme/engineering-workflows:2.0.0")
+bundle = musher.pull("musher-examples/code-review-kit:1.2.0")
# Select only the skills needed for this session
selection = bundle.select(skills=["researching-repos", "drafting-release-notes"])
# Export as a local Claude plugin with a namespaced plugin name.
-# Skills will be accessible as "team-workflows:researching-repos", etc.
-plugin = selection.export_claude_plugin("team-workflows", dest=Path("./plugins"))
+# Skills will be accessible as "code-review:researching-repos", etc.
+plugin = selection.export_claude_plugin("code-review", dest=Path("./plugins"))
print(f"Plugin exported to: {plugin.path}")
# Verify only the selected skills are present
diff --git a/examples/claude/install_project_skills.py b/examples/claude/install_project_skills.py
index 8d95109..fbccb20 100644
--- a/examples/claude/install_project_skills.py
+++ b/examples/claude/install_project_skills.py
@@ -13,13 +13,10 @@
import musher
-# NOTE: Bundle references below (e.g. "acme/engineering-workflows:2.0.0") are
-# placeholders. Replace with a real bundle ref from your Musher registry.
-
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
# or credential file. To override: musher.configure(token="your-token")
-bundle = musher.pull("acme/engineering-workflows:2.0.0")
+bundle = musher.pull("musher-examples/code-review-kit:1.2.0")
# Install specific skills to the project-level Claude skills directory.
# clean=True removes stale Musher-managed skill installs for this bundle
diff --git a/examples/openai/hosted_inline_skill.py b/examples/openai/hosted_inline_skill.py
index ebf5f79..31ba673 100644
--- a/examples/openai/hosted_inline_skill.py
+++ b/examples/openai/hosted_inline_skill.py
@@ -9,15 +9,12 @@
import musher
-# NOTE: Bundle references below (e.g. "acme/data-workflows:2.0.0") are
-# placeholders. Replace with a real bundle ref from your Musher registry.
-
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
# or credential file. To override: musher.configure(token="your-token")
async def main() -> None:
- bundle = musher.pull("acme/data-workflows:2.0.0")
+ bundle = musher.pull("musher-examples/data-workflows:1.0.0")
skill = bundle.skill("csv-insights")
inline = skill.export_openai_inline_skill()
diff --git a/examples/openai/local_shell_skill.py b/examples/openai/local_shell_skill.py
index 30af950..c360f6f 100644
--- a/examples/openai/local_shell_skill.py
+++ b/examples/openai/local_shell_skill.py
@@ -18,9 +18,6 @@
import musher
-# NOTE: Bundle references below (e.g. "acme/engineering-workflows:2.0.0") are
-# placeholders. Replace with a real bundle ref from your Musher registry.
-
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
# or credential file. To override: musher.configure(token="your-token")
@@ -65,12 +62,12 @@ async def __call__(self, request: ShellCommandRequest) -> ShellResult:
async def main() -> None:
- bundle = musher.pull("acme/engineering-workflows:2.0.0")
- skill = bundle.skill("repo-maintainer")
+ bundle = musher.pull("musher-examples/code-review-kit:1.2.0")
+ skill = bundle.skill("researching-repos")
local = skill.export_openai_local_skill(dest=PROJECT_DIR / ".musher" / "openai" / "skills")
agent = Agent(
- name="Repo Triage Assistant",
+ name="Repo Research Assistant",
model="gpt-4.1",
instructions="Use the local skill when it helps. Keep the answer concise and actionable.",
tools=[
@@ -83,7 +80,7 @@ async def main() -> None:
result = await Runner.run(
agent,
- "Use the repo-maintainer skill to inspect this repository and tell me the first onboarding issue I should fix.",
+ "Use the researching-repos skill to explore this repository and summarize its tech stack, structure, and conventions.",
)
print(result.final_output)
diff --git a/examples/pydantic_ai/dynamic_instructions.py b/examples/pydantic_ai/dynamic_instructions.py
index 9a4bcad..5b1d27d 100644
--- a/examples/pydantic_ai/dynamic_instructions.py
+++ b/examples/pydantic_ai/dynamic_instructions.py
@@ -10,13 +10,10 @@
import musher
-# NOTE: Bundle references below (e.g. "acme/prompt-library:1.2.0") are
-# placeholders. Replace with a real bundle ref from your Musher registry.
-
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
# or credential file. To override: musher.configure(token="your-token")
-bundle = musher.pull("acme/prompt-library:1.2.0")
+bundle = musher.pull("musher-examples/prompt-library:1.2.0")
agent = Agent("openai:gpt-4o")
diff --git a/examples/pydantic_ai/instructions_from_bundle.py b/examples/pydantic_ai/instructions_from_bundle.py
index 479b920..66dff6c 100644
--- a/examples/pydantic_ai/instructions_from_bundle.py
+++ b/examples/pydantic_ai/instructions_from_bundle.py
@@ -10,13 +10,10 @@
import musher
-# NOTE: Bundle references below (e.g. "acme/prompt-library:1.2.0") are
-# placeholders. Replace with a real bundle ref from your Musher registry.
-
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
# or credential file. To override: musher.configure(token="your-token")
-bundle = musher.pull("acme/prompt-library:1.2.0")
+bundle = musher.pull("musher-examples/prompt-library:1.2.0")
# Load versioned instructions from the bundle
instructions_text = bundle.prompt("system").text()
diff --git a/examples/pydantic_ai/structured_output.py b/examples/pydantic_ai/structured_output.py
index f857417..d72be67 100644
--- a/examples/pydantic_ai/structured_output.py
+++ b/examples/pydantic_ai/structured_output.py
@@ -11,9 +11,6 @@
import musher
-# NOTE: Bundle references below (e.g. "acme/prompt-library:1.2.0") are
-# placeholders. Replace with a real bundle ref from your Musher registry.
-
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
# or credential file. To override: musher.configure(token="your-token")
@@ -27,7 +24,7 @@ class IncidentSummary(BaseModel):
action_items: list[str]
-bundle = musher.pull("acme/prompt-library:1.2.0")
+bundle = musher.pull("musher-examples/prompt-library:1.2.0")
instructions_text = bundle.prompt("system").text()
# The agent returns an IncidentSummary instead of free-form text
diff --git a/lefthook.yml b/lefthook.yml
index f6f019e..b3b89f3 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -1,42 +1,10 @@
-# EXAMPLE USAGE:
-#
-# Refer for explanation to following link:
-# https://lefthook.dev/configuration/
-#
-# pre-push:
-# jobs:
-# - name: packages audit
-# tags:
-# - frontend
-# - security
-# run: yarn audit
-#
-# - name: gems audit
-# tags:
-# - backend
-# - security
-# run: bundle audit
-#
-# pre-commit:
-# parallel: true
-# jobs:
-# - run: yarn eslint {staged_files}
-# glob: "*.{js,ts,jsx,tsx}"
-#
-# - name: rubocop
-# glob: "*.rb"
-# exclude:
-# - config/application.rb
-# - config/routes.rb
-# run: bundle exec rubocop --force-exclusion -- {all_files}
-#
-# - name: govet
-# files: git ls-files -m
-# glob: "*.go"
-# run: go vet -- {files}
-#
-# - script: "hello.js"
-# runner: node
-#
-# - script: "hello.go"
-# runner: go run
+pre-commit:
+ parallel: true
+ jobs:
+ - name: ruff-format
+ glob: "*.py"
+ run: uv run ruff format --check {staged_files}
+
+ - name: ruff-lint
+ glob: "*.py"
+ run: uv run ruff check {staged_files}
diff --git a/pyproject.toml b/pyproject.toml
index e623f86..bfc395d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,6 +28,7 @@ dev = [
"pytest-asyncio>=1.0.0",
"pytest-cov>=4.0.0",
"respx>=0.20.0",
+ "pytest-xdist>=3.0",
]
[build-system]
@@ -43,9 +44,10 @@ asyncio_mode = "auto"
addopts = [
"--strict-markers",
"--strict-config",
+ "-n=auto",
"--cov=musher",
"--cov-report=term-missing",
- "--cov-fail-under=80",
+ "--cov-fail-under=90",
]
[tool.basedpyright]
diff --git a/src/musher/__init__.py b/src/musher/__init__.py
index 37049dc..1350d24 100644
--- a/src/musher/__init__.py
+++ b/src/musher/__init__.py
@@ -10,7 +10,7 @@
from musher._auth import resolve_registry_url
from musher._bundle import Asset, Bundle, Manifest, ManifestAsset, ResolveResult
-from musher._cache import BundleCache
+from musher._cache import BundleCache # internal; not re-exported in __all__
from musher._cache_info import CachedBundle, CachedBundleVersion, CacheInfo
from musher._client import AsyncClient, Client
from musher._config import MusherConfig, configure, get_config
diff --git a/src/musher/adapters/__init__.py b/src/musher/adapters/__init__.py
deleted file mode 100644
index 1164773..0000000
--- a/src/musher/adapters/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Framework adapters (LangChain, LlamaIndex, etc.) — future packages."""
diff --git a/uv.lock b/uv.lock
index a176aa8..cff7f72 100644
--- a/uv.lock
+++ b/uv.lock
@@ -641,6 +641,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
+[[package]]
+name = "execnet"
+version = "2.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
+]
+
[[package]]
name = "executing"
version = "2.2.1"
@@ -1453,6 +1462,7 @@ dev = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
+ { name = "pytest-xdist" },
{ name = "respx" },
{ name = "ruff" },
]
@@ -1476,6 +1486,7 @@ dev = [
{ name = "pytest", specifier = ">=8.3.0,<9.0.0" },
{ name = "pytest-asyncio", specifier = ">=1.0.0" },
{ name = "pytest-cov", specifier = ">=4.0.0" },
+ { name = "pytest-xdist", specifier = ">=3.0" },
{ name = "respx", specifier = ">=0.20.0" },
{ name = "ruff", specifier = ">=0.15.2" },
]
@@ -2166,6 +2177,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
+[[package]]
+name = "pytest-xdist"
+version = "3.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "execnet" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
+]
+
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"