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
115 changes: 77 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

**The package manager for AI agents.**

Share AI agent skills across your team like code packages — from any Git repo,
into Claude Code, Cursor, Codex, and more.
For teams who want to manage agent skills like software packages — the way npm,
PyPI, and uv manage code. Install skills from any Git repo into Claude Code,
Cursor, Codex, and more, then share them across your team like real dependencies.

[![PyPI](https://img.shields.io/pypi/v/agr?color=blue)](https://pypi.org/project/agr/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Expand All @@ -19,25 +20,58 @@ into Claude Code, Cursor, Codex, and more.

---

## Getting started
## Why agr

Install the CLI:
agr is for **teams** who want to manage their agent skills as seriously as they
manage their code — the way npm, PyPI, and uv manage software packages.

Skills make AI agents better at your work. But today they're copied around by
hand, drift between machines, and live in tool-specific folders. agr treats them
like real dependencies: declared in one manifest, locked to a version, installed
with one command, and identical for every teammate, on every machine, in every
tool.

That brings the same wins you already get from a package manager for code:

- **Version & pin.** `agr.lock` records the exact version of every skill, so a
skill that works today keeps working tomorrow — no silent upstream changes
breaking your agents. Upgrade on purpose, when you choose, with `agr upgrade`.
- **Distribute effortlessly.** Publishing a skill is just pushing to a Git repo;
installing one is `agr add owner/repo/skill`. No registry to set up, no files
to email around.
- **One source of truth for the team.** The skills your agents use are part of
your repo — reviewed in PRs, versioned in Git, and shared like any other
dependency. Everyone runs the same skills, so your agents behave consistently
across the whole team.
- **Onboard in one command.** A new teammate clones the repo, runs `agr sync`,
and their agents are set up exactly like everyone else's — same skills, same
standards, day one.

## Install

```bash
uv tool install agr
```

Install your first skill:
---

## What you can do with it

Five things. That's the whole tool.

### 1. Install a skill from a Git repo

```bash
agr add anthropics/skills/pdf
```

Handles follow the pattern `owner/repo/skill` — pointing to a directory inside
a GitHub repo. `anthropics/skills/pdf` means the `pdf/` directory inside
[github.com/anthropics/skills](https://github.com/anthropics/skills).
Handles are just a path into GitHub: **`owner/repo/skill`**. `anthropics/skills/pdf`
is the `pdf/` directory inside
[github.com/anthropics/skills](https://github.com/anthropics/skills). Any public
repo works — no registry, no publishing step.

Then invoke it in your AI tool:
`agr add` auto-creates `agr.toml`, detects which AI tools you use, and installs
the skill into each. Then invoke it in your tool:

| Tool | Invoke with |
|------|-------------|
Expand All @@ -48,69 +82,74 @@ Then invoke it in your AI tool:
| GitHub Copilot | `/pdf` |
| Antigravity | *(via IDE)* |

No setup required — `agr add` auto-creates `agr.toml` and detects which tools
you use.
### 2. Use your own local skills

---
Point at a directory on disk instead of a repo:

## Built for teams
```bash
agr add ./skills/my-internal-skill
```

Great for skills you're still writing, or ones that never leave your codebase.
They sync into every tool exactly like remote skills.

agr is opinionated: skill directories (`.claude/skills/`, `.cursor/skills/`, …)
are build artifacts — like `.venv/` or `node_modules/`. Add them to `.gitignore`.
Commit `agr.toml` and `agr.lock` instead. `agr sync` rebuilds the environment
from the manifest on every machine.
### 3. Share one skill environment with your team

`.claude/skills/`, `.cursor/skills/`, … are build artifacts — like `.venv/` or
`node_modules/`. Add them to `.gitignore`. Commit **`agr.toml`** and
**`agr.lock`** instead:

```toml
tools = ["claude", "cursor"]

dependencies = [
{handle = "anthropics/skills/pdf", type = "skill"},
{handle = "anthropics/skills/frontend-design", type = "skill"},
{path = "./skills/my-internal-skill", type = "skill"},
]
```

A new teammate clones the repo and runs:

```bash
agr sync # Like npm install, but for AI agents
agr sync # like `npm install`, but for AI agents
```

New teammate? `agr sync` and they're productive on day one — same skills,
same standards, every tool.
Now everyone has the same skills, the same standards, in every tool.

---

## Keep skills up to date
### 4. Keep skills up to date

```bash
agr upgrade # all skills
agr upgrade pdf # one skill
agr upgrade pdf # just one
```

---
Re-fetches skills at their latest upstream version and updates `agr.lock`.

## Example skills
### 5. Try a skill without installing it

```bash
agr add anthropics/skills/pdf # Read, extract, create PDFs
agr add anthropics/skills/frontend-design # Production-grade interfaces
agr add anthropics/skills/claude-api # Build apps with the Claude API
agr add anthropics/skills/skill-creator # Create, modify, and improve skills
agrx anthropics/skills/pdf
```

Downloads and runs a skill once, then throws it away — nothing added to
`agr.toml`, nothing left behind.

---

## All commands

| Command | Description |
|---------|-------------|
| `agr add <handle>` | Install a skill |
| Command | What it does |
|---------|--------------|
| `agr add <handle\|path>` | Install a skill and add it to `agr.toml` |
| `agr remove <handle>` | Uninstall a skill |
| `agr sync` | Install all from `agr.toml` |
| `agr upgrade [handle...]` | Re-fetch deps at latest version |
| `agr sync` | Install everything in `agr.toml` |
| `agr upgrade [handle...]` | Re-fetch skills at their latest version |
| `agr list` | Show installed skills |
| `agrx <handle>` | Run a skill temporarily without installing |
| `agrx <handle>` | Run a skill once, without installing |

Add `-g` to `add`, `remove`, `sync`, or `list` for global skills (available in
all projects).
Add `-g` to `add`, `remove`, `sync`, or `list` to manage **global** skills,
available across all your projects.

---

Expand Down
34 changes: 19 additions & 15 deletions agr/commands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Dependency,
)
from agr.console import get_console
from agr.features import feature_enabled
from agr.exceptions import (
INSTALL_ERROR_TYPES,
AgrError,
Expand Down Expand Up @@ -68,7 +69,7 @@ def _detect_local_type(source_path: Path) -> str:
If neither marker exists, checks for a [package] section in agr.toml.
If nothing matches, defaults to skill (existing behaviour).
"""
has_ralph = is_valid_ralph_dir(source_path)
has_ralph = is_valid_ralph_dir(source_path) and feature_enabled("ralph")
has_skill = is_valid_skill_dir(source_path)

if has_ralph and has_skill:
Expand Down Expand Up @@ -192,20 +193,21 @@ def _install_dependency(
except SkillNotFoundError:
pass

try:
installed_path, install_result = fetch_and_install_ralph(
handle,
repo_root,
overwrite,
resolver=resolver,
source=source,
default_repo=default_repo,
)
return AddInstallResult(
[str(installed_path)], install_result, DEPENDENCY_TYPE_RALPH
)
except RalphNotFoundError:
pass
if feature_enabled("ralph"):
try:
installed_path, install_result = fetch_and_install_ralph(
handle,
repo_root,
overwrite,
resolver=resolver,
source=source,
default_repo=default_repo,
)
return AddInstallResult(
[str(installed_path)], install_result, DEPENDENCY_TYPE_RALPH
)
except RalphNotFoundError:
pass

return _install_package(
handle,
Expand Down Expand Up @@ -270,6 +272,8 @@ def _install_package(
(DEPENDENCY_TYPE_PACKAGE, entry) for entry in expanded.package_entries
]
for dep in expanded.dependencies:
if dep.is_ralph and not feature_enabled("ralph"):
continue
sub_handle = dep.to_parsed_handle(config.default_owner)
sub_source = dep.resolve_source_name(config.default_source)
if dep.is_ralph:
Expand Down
10 changes: 10 additions & 0 deletions agr/commands/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
find_config,
require_repo_root,
)
from agr.features import feature_enabled
from agr.package import ExpandedDeps, detect_conflicts, expand_packages
from agr.console import error_exit, get_console, print_error
from agr.exceptions import (
Expand Down Expand Up @@ -604,6 +605,11 @@ def _classify_dependencies(
results[index] = SyncResult.up_to_date()
continue

if dep.is_ralph and not feature_enabled("ralph"):
# Ralph install is gated off: skip silently as if absent.
results[index] = SyncResult.up_to_date()
continue

if dep.is_ralph:
# Ralphs are tool-agnostic: check project-level ralphs dir.
if not forced and is_ralph_installed(
Expand Down Expand Up @@ -893,6 +899,10 @@ def _sync_dep_from_lockfile(
handle, source_name = dep.resolve(config.default_source, config.default_owner)
is_ralph_dep = dep.is_ralph

# Ralph install is gated off: skip silently as if absent.
if is_ralph_dep and not feature_enabled("ralph"):
return SyncResult.up_to_date()

# Check installation status — initialise tools_needing_install
# unconditionally so it is always bound regardless of code path.
tools_needing_install: list[ToolConfig] = []
Expand Down
36 changes: 36 additions & 0 deletions agr/features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Environment-variable-backed feature flags.

A small registry maps feature names to the environment variables that enable
them. A feature is *off* unless its env var is set to a recognised truthy
value. The registry keeps adding a new gated feature to a single line.

Truthy values (case-insensitive, surrounding whitespace ignored):
``1``, ``true``, ``yes``, ``on``. Anything else — including unset — is off.
"""

import os

# Feature name -> environment variable that enables it.
_FEATURE_ENV_VARS: dict[str, str] = {
"ralph": "AGR_ENABLE_RALPH",
}

# Recognised truthy env-var values (compared lowercase, stripped).
_TRUTHY_VALUES = frozenset({"1", "true", "yes", "on"})


def _is_truthy(value: str | None) -> bool:
"""Return True if an env-var value counts as enabling a feature."""
if value is None:
return False
return value.strip().lower() in _TRUTHY_VALUES


def feature_enabled(name: str) -> bool:
"""Return whether the named feature is enabled via its env var.

Raises:
KeyError: if ``name`` is not a registered feature.
"""
env_var = _FEATURE_ENV_VARS[name]
return _is_truthy(os.environ.get(env_var))
7 changes: 7 additions & 0 deletions agr/ralph_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
InvalidLocalPathError,
RalphNotFoundError,
)
from agr.features import feature_enabled
from agr.handle import (
INSTALLED_NAME_SEPARATOR,
ParsedHandle,
Expand Down Expand Up @@ -273,6 +274,12 @@ def fetch_and_install_ralph(
Returns:
Tuple of (installed path, InstallResult with lockfile metadata).
"""
# Defense-in-depth: with the ralph feature off, no caller can install a
# ralph. Raise the same not-found error a missing ralph would, so nothing
# reveals that a feature flag is involved.
if not feature_enabled("ralph"):
raise RalphNotFoundError(handle.name)

if repo_root is None:
raise AgrError("repo_root is required for ralph installation")

Expand Down
Loading
Loading