Skip to main content

Install artifacts (skills, rules, agents, commands) into coding agents

Project description

๐ŸŽ’ loadout

The package manager for AI coding agent artifacts.

Stop manually copying skills, rules, agents, and commands into every coding agent's config directory. loadout handles discovery, transformation, and installation across Claude Code, Cursor, and OpenCode โ€” so your CLI or project setup script doesn't have to.

๐Ÿ“ฆ Installation

# uv (recommended)
uv add loadout

# pip
pip install loadout

# With interactive agent selection prompt
uv add "loadout[interactive]"
pip install "loadout[interactive]"

๐Ÿค” Why loadout?

Every team building on top of AI coding agents hits the same problem: distributing custom skills, rules, and commands to developers' machines. The config directories differ, the file formats differ, and the logic for "install this skill to that agent" gets copy-pasted across tooling.

loadout extracts that logic into a single, typed, tested library with an extensible adapter pattern. You write your artifacts once. loadout puts them where they belong.

  • ๐ŸŒ Agent-agnostic โ€” one artifact definition works across all supported agents
  • ๐Ÿ” Auto-detection โ€” discovers which agents are installed on the system
  • ๐Ÿ“ Convention + configuration โ€” scan for marker files, or define a manifest
  • ๐Ÿ”Œ Adapter pattern โ€” add support for new agents without touching core logic
  • ๐Ÿช Lifecycle hooks โ€” plug in your own logging, progress bars, or analytics
  • ๐Ÿ›ก๏ธ Fully typed โ€” strict mypy, Pydantic models, protocol classes

โšก Quick start

๐Ÿš€ Install everything to every detected agent

from loadout import install_all

summary = install_all("./my-artifacts", force=True)

print(f"โœ… Installed: {len(summary.installed)}")
print(f"โญ๏ธ  Skipped:   {len(summary.skipped)}")
print(f"โŒ Failed:    {len(summary.failed)}")

๐Ÿ’ฌ Interactive mode (checkbox prompt)

uv add "loadout[interactive]"
from loadout import install_interactive

summary = install_interactive("./my-artifacts")

๐ŸŽ›๏ธ Full control

from loadout import discover_artifacts, detect_agents, install

artifacts = discover_artifacts("./my-artifacts")
agents = detect_agents()
summary = install(artifacts, agents, force=True)

Three tiers โ€” pick the one that fits your UX. ๐ŸŽฏ


๐Ÿค– Supported agents

Agent Config dir Skills Rules Agents Commands
Claude Code ~/.claude/ โœ… โœ… โœ… โœ…
Cursor ~/.cursor/ โœ… โœ… (.mdc) โ€” โ€”
OpenCode ~/.opencode/ โœ… โ€” โ€” โœ…

loadout auto-detects which agents are present and only installs to agents that support each artifact type. Unsupported combinations are cleanly skipped. โœจ


๐Ÿ”Ž Artifact discovery

๐Ÿ“‚ Convention-based (marker files)

Drop marker files into your artifact directories and loadout will find them:

my-artifacts/
  login-skill/
    SKILL.md              # โ† marker: this directory is a skill
    helper.py
    utils.py
  security/
    auth-rule/
      RULE.md             # โ† marker: this file is a rule
  setup-agent/
    AGENT.md              # โ† marker: this file is an agent
  deploy/
    COMMAND.md            # โ† marker: this file is a command

Marker files double as the artifact content โ€” the SKILL.md is the skill. Categories are derived from directory structure (security/auth-rule/ โ†’ category security).

๐Ÿ“‹ Manifest-based (loadout.yaml)

For explicit control, add a loadout.yaml to the root of your artifacts directory:

artifacts:
  - name: login-skill
    type: skill
    path: login-skill

  - name: auth-rule
    type: rule
    path: security/auth-rule/RULE.md
    category: security
    description: "Enforces authentication checks"

When a manifest is present, marker-file scanning is skipped entirely โ€” you have full control over what gets installed.

๐Ÿ“ Frontmatter support

Artifact files can include YAML frontmatter for metadata:

---
description: Handles user login flows
globs:
  - "src/auth/**"
always_apply: true
---

# Login Skill

Your skill content here...

๐Ÿ”Œ Custom adapters

Need to support a new coding agent? Implement the AgentAdapter interface and register it:

from pathlib import Path
from loadout import (
    AgentAdapter,
    Artifact,
    ArtifactType,
    DetectedAgent,
    InstallResult,
    get_default_registry,
    install_all,
)

class WindsurfAdapter(AgentAdapter):
    @property
    def agent_name(self) -> str:
        return "windsurf"

    @property
    def display_name(self) -> str:
        return "Windsurf"

    @property
    def config_dir_name(self) -> str:
        return ".windsurf"

    def supported_artifact_types(self) -> set[ArtifactType]:
        return {ArtifactType.SKILL, ArtifactType.RULE}

    def detect(self) -> DetectedAgent | None:
        config_dir = Path.home() / self.config_dir_name
        if config_dir.is_dir():
            return DetectedAgent(
                name=self.agent_name,
                config_dir=config_dir,
                display_name=self.display_name,
            )
        return None

    def get_target_path(self, artifact: Artifact, config_dir: Path) -> Path:
        # Your path resolution logic
        ...

    def transform_content(self, artifact: Artifact, content: str) -> str:
        # Your content transformation logic
        return content

    def transform_filename(self, artifact: Artifact, filename: str) -> str:
        return filename

    def install(self, artifact: Artifact, agent: DetectedAgent, force: bool = False) -> InstallResult:
        # Your install logic
        ...

# Register and use ๐ŸŽ‰
registry = get_default_registry()
registry.register(WindsurfAdapter())

summary = install_all("./my-artifacts", registry=registry)

The adapter pattern means core loadout never needs to change when new agents appear. ๐Ÿงฉ


๐Ÿช Lifecycle callbacks

Hook into every stage of the installation process for logging, progress bars, analytics, or custom error handling:

from loadout import LoadoutCallbacks, Artifact, DetectedAgent, InstallResult, install_all

class RichCallbacks:
    """Example: pretty-print progress with Rich."""

    def on_artifact_discovered(self, artifact: Artifact) -> None:
        print(f"  ๐Ÿ”Ž Found {artifact.artifact_type.value}: {artifact.name}")

    def on_agent_detected(self, agent: DetectedAgent) -> None:
        print(f"  ๐Ÿค– Detected agent: {agent.display_name}")

    def on_install_started(self, artifact: Artifact, agent: DetectedAgent) -> None:
        print(f"  โณ Installing {artifact.name} โ†’ {agent.display_name}...")

    def on_install_complete(self, result: InstallResult) -> None:
        print(f"  โœ… Installed to {result.target_path}")

    def on_install_skipped(self, result: InstallResult) -> None:
        print(f"  โญ๏ธ  Skipped: {result.error}")

    def on_install_failed(self, result: InstallResult) -> None:
        print(f"  ๐Ÿ’ฅ FAILED: {result.error}")

summary = install_all("./my-artifacts", callbacks=RichCallbacks())

Only override the hooks you care about โ€” the LoadoutCallbacks protocol defines the full interface, and NoOpCallbacks provides a ready-made base with no-op defaults.


๐Ÿ“– API reference

โš™๏ธ Top-level functions

Function Description
install_all(source_dir, force, registry, callbacks) Discover artifacts, detect agents, install everything
install_interactive(source_dir, force, registry, callbacks) Same as above with interactive agent selection
install(artifacts, agents, force, registry, callbacks) Install specific artifacts to specific agents
discover_artifacts(source_dir) Scan a directory and return a list of Artifact objects
detect_agents(registry) Detect installed coding agents
get_default_registry() Get the built-in adapter registry

๐Ÿงฑ Models

Model Description
Artifact A discovered artifact (name, type, source path, category, frontmatter)
ArtifactType Enum: SKILL, RULE, AGENT, COMMAND
DetectedAgent An agent found on the system (name, config dir, display name)
InstallResult Result of a single artifact install (status, target path, error)
InstallSummary Batch result with .installed, .skipped, .failed, .already_existed
Manifest Parsed loadout.yaml manifest

๐Ÿšจ Exceptions

Exception Description
LoadoutError Base exception for all loadout errors
ArtifactNotFoundError Source artifact path does not exist
ManifestError Invalid loadout.yaml
InstallError Installation failed
AdapterNotFoundError No adapter registered for the given agent
AdapterAlreadyRegisteredError Adapter name collision
TransformError Content transformation failed

๐Ÿ› ๏ธ Development

# Clone and install with all extras
git clone https://github.com/nickmaccarthy/loadout.git
cd loadout

# uv (recommended)
uv sync --all-extras

# pip
pip install -e ".[dev,interactive]"

# Set up pre-commit hooks
uv run pre-commit install

# Run all pre-commit checks (ruff, mypy, formatting, etc.)
uv run pre-commit run --all-files

# Run tests
uv run pytest

# Run tests with coverage
uv run pytest --cov=loadout --cov-report=term-missing

๐Ÿ’ก Pre-commit hooks run automatically on every git commit, catching lint errors, type issues, and formatting problems before they hit CI.


๐Ÿ“‹ Requirements

๐Ÿ“„ License

MIT โ€” see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

loadout-0.3.0.tar.gz (19.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

loadout-0.3.0-py3-none-any.whl (18.7 kB view details)

Uploaded Python 3

File details

Details for the file loadout-0.3.0.tar.gz.

File metadata

  • Download URL: loadout-0.3.0.tar.gz
  • Upload date:
  • Size: 19.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for loadout-0.3.0.tar.gz
Algorithm Hash digest
SHA256 0f6253d09f2c39bed48f653f749c8644076b7ced060e90cd1f09d8989137dd7e
MD5 898857fecc7d268f16f7290ff1fe191c
BLAKE2b-256 2a9cfc3f6bbdbe5151208b87ebec7742597db5b68492e6b71a1314963e827d3b

See more details on using hashes here.

Provenance

The following attestation bundles were made for loadout-0.3.0.tar.gz:

Publisher: release.yml on nickmaccarthy/loadout

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file loadout-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: loadout-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 18.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for loadout-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ec22176191239c8d2e8ac225b645ad45af480f8ee316499d8b825d0f25e540d6
MD5 2354c439b407a44876ca553966f9a570
BLAKE2b-256 82d1f0fbb6f9f7edbaf55bba37d3362f7a828b2de0a2227d5c22fc08928f6dd5

See more details on using hashes here.

Provenance

The following attestation bundles were made for loadout-0.3.0-py3-none-any.whl:

Publisher: release.yml on nickmaccarthy/loadout

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page