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
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,44 @@

All notable changes to this project will be documented in this file.

## Capacium v0.11.0 — Phase 2: Capacium v2 Redesign (2026-05-24)

### New Features

- **`cap export-mcp`** (CAP-008): Export capability manifests as MCP server
descriptors. Generates standardized `serverInfo`, `capabilities/tools`,
and `transport` sections from `capability.yaml`.

- **`cap export-a2a`** (CAP-008): Export capability manifests as A2A agent
cards. Generates `skills`, `provider`, and `capabilities` sections for
Google A2A protocol compatibility.

- **`cap adapt`** (CAP-011): Framework adaptation layer with pluggable
registry. Adapts capability manifests to target frameworks (mcp-server,
a2a-agent, claude-desktop) using capability-aware transformation pipelines.

- **Standards Exporters** (CAP-008): New `capacium.exporters` package with
`MCPExporter` and `A2AExporter`. Abstract `BaseExporter` supports
`export()`, `can_export()`, and `export_json()` methods. 16 tests.

- **Adaptation Registry** (CAP-011): New `capacium.adaptation` package with
`AdaptationRegistry` (3 built-in targets) and `CapabilityAdapter` for
framework-agnostic capability transformation. 38 tests.

- **Manifest triggers field** (CAP-003): New `triggers:` section in
`capability.yaml` for event-driven capability activation patterns.

- **Manifest pricing field** (CAP-004): New `pricing:` section in
`capability.yaml` supporting free/freemium/paid models with tier
definitions and usage limits.

- **Resource Kind 5-layer schema** (CAP-002): Progressive disclosure schema
for resources — from simple key-value to full conditional evaluation with
`ConditionEvaluator`.

- **Broad resource support**: Resource kind detection, condition evaluation,
and 5-layer progressive resource schema integrated into CLI.

## Capacium v1.0.0-dev — Phase 1 (2026-05-11)

### Deprecations
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ Works fully offline from local paths. The Exchange layer (separate repo) adds re
You can install Capacium globally in an isolated environment using `pipx`.

```bash
pipx install git+https://github.com/Capacium/capacium.git@v0.10.25
pipx install git+https://github.com/Capacium/capacium.git@v0.11.0

# Or with optional signing and YAML support:
pipx install "capacium[yaml,signing] @ git+https://github.com/Capacium/capacium.git@v0.10.25"
pipx install "capacium[yaml,signing] @ git+https://github.com/Capacium/capacium.git@v0.11.0"
```

*(Note: PyPI publishing `pip install capacium` is pending organization approval and currently unavailable).*
Expand All @@ -35,7 +35,7 @@ If you don't use Python, you can download standalone executables directly from t
### 3. Docker (GHCR)
Run Capacium safely in a container with your directories mounted:
```bash
docker run --rm -v ~/.capacium:/root/.capacium -v $(pwd):/workspace ghcr.io/capacium/cap:0.10.25
docker run --rm -v ~/.capacium:/root/.capacium -v $(pwd):/workspace ghcr.io/capacium/cap:0.11.0
```

### 4. macOS / Linux (Homebrew)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "capacium"
version = "0.10.25"
version = "0.11.0"
description = "Capability Packaging System — agent-agnostic packaging for AI agent capabilities"
readme = "README.md"
authors = [
Expand Down
4 changes: 4 additions & 0 deletions src/capacium/adaptation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .adapter import CapabilityAdapter
from .registry import AdaptationRegistry

__all__ = ["CapabilityAdapter", "AdaptationRegistry"]
121 changes: 121 additions & 0 deletions src/capacium/adaptation/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Capability Adapter — transforms capabilities between framework formats.

Uses exporters for format conversion and the adaptation registry
for target-specific configuration.
"""
from typing import Any, Dict, List, Optional

from ..manifest import Manifest
from ..exporters import MCPExporter, A2AExporter
from .registry import AdaptationRegistry, AdaptationTarget


class AdaptationError(Exception):
"""Raised when adaptation fails."""
pass


class CapabilityAdapter:
"""Adapts capabilities to target framework formats."""

def __init__(self):
self._registry = AdaptationRegistry()
self._exporters = {
"mcp-server": MCPExporter(),
"a2a-agent": A2AExporter(),
}

@property
def registry(self) -> AdaptationRegistry:
return self._registry

def adapt(
self,
manifest: Manifest,
target: str,
options: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Adapt a manifest to target framework format.

Args:
manifest: Source capability manifest
target: Target framework name (e.g. "mcp-server", "a2a-agent")
options: Optional adaptation options (transport, etc.)

Returns:
Adapted output as dict

Raises:
AdaptationError: If adaptation not possible
"""
target_info = self._registry.get(target)
if target_info is None:
available = ", ".join(self._registry.list_targets())
raise AdaptationError(
f"Unknown adaptation target '{target}'. Available: {available}"
)

# Check if we have an exporter for this target
exporter = self._exporters.get(target)
if exporter is not None:
if not exporter.can_export(manifest):
raise AdaptationError(
f"Cannot adapt manifest kind '{manifest.kind}' to '{target}'"
)
result = exporter.export(manifest)
else:
# Generic adaptation via target info
result = self._generic_adapt(manifest, target_info)

# Apply target-specific options
if options:
result = self._apply_options(result, target_info, options)

return result

def can_adapt(self, manifest: Manifest, target: str) -> bool:
"""Check if a manifest can be adapted to the target."""
target_info = self._registry.get(target)
if target_info is None:
return False
exporter = self._exporters.get(target)
if exporter is not None:
return exporter.can_export(manifest)
return True # Generic adaptation always possible

def list_targets(self, manifest: Optional[Manifest] = None) -> List[str]:
"""List available adaptation targets, optionally filtered by manifest compatibility."""
if manifest is None:
return self._registry.list_targets()
return [t for t in self._registry.list_targets() if self.can_adapt(manifest, t)]

def _generic_adapt(self, manifest: Manifest, target: AdaptationTarget) -> Dict[str, Any]:
"""Generic adaptation when no specific exporter exists."""
result: Dict[str, Any] = {
"name": manifest.name,
"version": manifest.version,
"description": manifest.description,
"kind": manifest.kind,
"adapted_from": "capacium",
"target": target.name,
}
if target.supports_tools and manifest.capabilities:
result["tools"] = [
{"name": c.get("name", ""), "description": c.get("description", "")}
for c in manifest.capabilities
]
if manifest.runtimes:
result["runtime"] = manifest.runtimes
return result

def _apply_options(
self, result: Dict[str, Any], target: AdaptationTarget, options: Dict[str, Any]
) -> Dict[str, Any]:
"""Apply adaptation options to result."""
if target.requires_transport and "transport" in options:
result["transport"] = options["transport"]
if "command" in options:
result["command"] = options["command"]
if "args" in options:
result["args"] = options["args"]
return result
58 changes: 58 additions & 0 deletions src/capacium/adaptation/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Registry of supported adaptation targets.

Each target describes a framework + what kind of output it needs.
"""
from dataclasses import dataclass
from typing import Dict, List, Optional


@dataclass
class AdaptationTarget:
"""Describes a target framework for adaptation."""
name: str # e.g. "mcp-server", "a2a-agent", "claude-desktop"
description: str = ""
output_format: str = "json" # json, yaml, toml
requires_transport: bool = False # needs transport config (e.g. MCP)
supports_tools: bool = True
supports_resources: bool = True
supports_prompts: bool = False


class AdaptationRegistry:
"""Registry of known adaptation targets."""

def __init__(self):
self._targets: Dict[str, AdaptationTarget] = {}
self._register_defaults()

def _register_defaults(self):
"""Register built-in adaptation targets."""
self.register(AdaptationTarget(
name="mcp-server",
description="Model Context Protocol server descriptor",
requires_transport=True,
supports_prompts=True,
))
self.register(AdaptationTarget(
name="a2a-agent",
description="Google A2A agent card",
supports_prompts=False,
))
self.register(AdaptationTarget(
name="claude-desktop",
description="Claude Desktop MCP config entry",
output_format="json",
requires_transport=True,
))

def register(self, target: AdaptationTarget) -> None:
self._targets[target.name] = target

def get(self, name: str) -> Optional[AdaptationTarget]:
return self._targets.get(name)

def list_targets(self) -> List[str]:
return list(self._targets.keys())

def all(self) -> List[AdaptationTarget]:
return list(self._targets.values())
22 changes: 20 additions & 2 deletions src/capacium/adapters/opencode.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,23 @@ def __init__(self):
self.storage = StorageManager()
self.symlink_manager = SymlinkManager()
self.opencode_skills_dir = Path.home() / ".opencode" / "skills"
self.commands_dir = Path.home() / ".config" / "opencode" / "commands"
self.opencode_skills_dir.mkdir(parents=True, exist_ok=True)
self.commands_dir.mkdir(parents=True, exist_ok=True)

def install_skill(self, cap_name: str, version: str, source_dir: Path, owner: str = "global") -> bool:
package_dir = ensure_package_dir(self.storage, cap_name, version, source_dir, owner=owner)

link_path = self.opencode_skills_dir / _cap_id(cap_name, owner)
success = self.symlink_manager.create_symlink(package_dir, link_path)
skill_success = self.symlink_manager.create_symlink(package_dir, link_path)
command_success = self._create_command_link(cap_name, package_dir)

metadata = self._extract_capability_metadata(package_dir)
metadata_path = package_dir / ".capacium-meta.json"
with open(metadata_path, "w") as f:
json.dump(metadata, f, indent=2)

return success
return skill_success and command_success

def remove_skill(self, cap_name: str, owner: str = "global") -> bool:
link_path = self.opencode_skills_dir / _cap_id(cap_name, owner)
Expand All @@ -50,12 +53,20 @@ def remove_skill(self, cap_name: str, owner: str = "global") -> bool:
shutil.rmtree(link_path)
else:
link_path.unlink()
command_path = self.commands_dir / f"{cap_name}.md"
if command_path.exists() or command_path.is_symlink():
if command_path.is_symlink():
self.symlink_manager.remove_symlink(command_path)
else:
command_path.unlink()
return True

def capability_exists(self, cap_name: str, owner: str = "global") -> bool:
link_path = self.opencode_skills_dir / _cap_id(cap_name, owner)
if link_path.exists() and link_path.is_symlink():
return True
if (self.commands_dir / f"{cap_name}.md").exists():
return True

config_path = Path.home() / ".config" / "opencode" / "opencode.json"
server_key = McpConfigPatcher.build_server_key(cap_name, owner)
Expand Down Expand Up @@ -124,6 +135,13 @@ def get_capability_metadata(self, cap_name: str) -> Optional[Dict[str, Any]]:
return json.load(f)
return None

def _create_command_link(self, cap_name: str, package_dir: Path) -> bool:
command_source = package_dir / "SKILL.md"
if not command_source.exists():
return True
command_path = self.commands_dir / f"{cap_name}.md"
return self.symlink_manager.create_symlink(command_source, command_path)

def _extract_capability_metadata(self, cap_dir: Path) -> Dict[str, Any]:
metadata = {
"name": cap_dir.parent.name,
Expand Down
Loading
Loading