capacium 0.15.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. capacium/__init__.py +13 -0
  2. capacium/adaptation/__init__.py +4 -0
  3. capacium/adaptation/adapter.py +121 -0
  4. capacium/adaptation/registry.py +58 -0
  5. capacium/adapters/__init__.py +124 -0
  6. capacium/adapters/aider.py +91 -0
  7. capacium/adapters/antigravity.py +78 -0
  8. capacium/adapters/base.py +61 -0
  9. capacium/adapters/capability_adapter.py +310 -0
  10. capacium/adapters/chainlit.py +49 -0
  11. capacium/adapters/cherry_studio.py +49 -0
  12. capacium/adapters/claude_code.py +112 -0
  13. capacium/adapters/claude_desktop.py +147 -0
  14. capacium/adapters/cline.py +51 -0
  15. capacium/adapters/codex.py +111 -0
  16. capacium/adapters/continue_dev.py +134 -0
  17. capacium/adapters/copilot.py +96 -0
  18. capacium/adapters/cursor.py +152 -0
  19. capacium/adapters/desktop_commander.py +49 -0
  20. capacium/adapters/gemini_cli.py +122 -0
  21. capacium/adapters/goose.py +92 -0
  22. capacium/adapters/hermes.py +77 -0
  23. capacium/adapters/junie.py +109 -0
  24. capacium/adapters/langchain_bridge.py +149 -0
  25. capacium/adapters/librechat.py +51 -0
  26. capacium/adapters/mcp_config_patcher.py +562 -0
  27. capacium/adapters/nextchat.py +49 -0
  28. capacium/adapters/openclaw.py +77 -0
  29. capacium/adapters/opencode.py +218 -0
  30. capacium/adapters/qwen.py +77 -0
  31. capacium/adapters/roo_code.py +50 -0
  32. capacium/adapters/sourcegraph_cody.py +49 -0
  33. capacium/adapters/stub_adapters.py +77 -0
  34. capacium/adapters/windsurf.py +49 -0
  35. capacium/adapters/zed.py +49 -0
  36. capacium/cli.py +977 -0
  37. capacium/commands/__init__.py +0 -0
  38. capacium/commands/_resolve.py +142 -0
  39. capacium/commands/adapt.py +103 -0
  40. capacium/commands/block_status.py +73 -0
  41. capacium/commands/browse.py +762 -0
  42. capacium/commands/compare.py +347 -0
  43. capacium/commands/doctor.py +527 -0
  44. capacium/commands/export.py +119 -0
  45. capacium/commands/hold.py +112 -0
  46. capacium/commands/info.py +408 -0
  47. capacium/commands/init.py +547 -0
  48. capacium/commands/install.py +1613 -0
  49. capacium/commands/key.py +57 -0
  50. capacium/commands/license.py +123 -0
  51. capacium/commands/list_capabilities.py +135 -0
  52. capacium/commands/lock.py +115 -0
  53. capacium/commands/marketplace.py +45 -0
  54. capacium/commands/package.py +115 -0
  55. capacium/commands/policy.py +214 -0
  56. capacium/commands/publish.py +241 -0
  57. capacium/commands/registry.py +98 -0
  58. capacium/commands/registry_cmd.py +175 -0
  59. capacium/commands/remove.py +359 -0
  60. capacium/commands/repair.py +476 -0
  61. capacium/commands/runtimes_cmd.py +61 -0
  62. capacium/commands/sandbox.py +129 -0
  63. capacium/commands/search.py +634 -0
  64. capacium/commands/sign.py +85 -0
  65. capacium/commands/skills_mcp.py +235 -0
  66. capacium/commands/submit.py +110 -0
  67. capacium/commands/update.py +293 -0
  68. capacium/commands/validate.py +324 -0
  69. capacium/commands/verify.py +153 -0
  70. capacium/conditions.py +265 -0
  71. capacium/exporters/__init__.py +4 -0
  72. capacium/exporters/a2a_exporter.py +57 -0
  73. capacium/exporters/base.py +31 -0
  74. capacium/exporters/mcp_exporter.py +52 -0
  75. capacium/fingerprint.py +54 -0
  76. capacium/framework_detector.py +262 -0
  77. capacium/index.py +420 -0
  78. capacium/manifest.py +331 -0
  79. capacium/migrations/__init__.py +1 -0
  80. capacium/models.py +199 -0
  81. capacium/registry.py +542 -0
  82. capacium/registry_client.py +476 -0
  83. capacium/runtimes.py +525 -0
  84. capacium/signing.py +302 -0
  85. capacium/skills_mcp_wrapper.py +310 -0
  86. capacium/storage.py +171 -0
  87. capacium/symlink_manager.py +74 -0
  88. capacium/sync.py +170 -0
  89. capacium/taxonomy.py +237 -0
  90. capacium/telemetry.py +105 -0
  91. capacium/tui_hints.py +189 -0
  92. capacium/ui.py +346 -0
  93. capacium/utils/__init__.py +0 -0
  94. capacium/utils/config.py +205 -0
  95. capacium/utils/copytree.py +73 -0
  96. capacium/utils/errors.py +22 -0
  97. capacium/utils/mcp_probe.py +91 -0
  98. capacium/utils/project_scope.py +25 -0
  99. capacium/utils/table.py +150 -0
  100. capacium/utils/toml_compat.py +6 -0
  101. capacium/versioning.py +136 -0
  102. capacium-0.15.0.dist-info/METADATA +224 -0
  103. capacium-0.15.0.dist-info/RECORD +107 -0
  104. capacium-0.15.0.dist-info/WHEEL +5 -0
  105. capacium-0.15.0.dist-info/entry_points.txt +3 -0
  106. capacium-0.15.0.dist-info/licenses/LICENSE +201 -0
  107. capacium-0.15.0.dist-info/top_level.txt +1 -0
capacium/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ Capacium — Capability Packaging System
3
+ Agent-agnostic, manifest-first packaging for AI agent capabilities.
4
+ """
5
+
6
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
7
+
8
+ try:
9
+ __version__ = _pkg_version("capacium")
10
+ except PackageNotFoundError:
11
+ __version__ = "0.0.0+unknown"
12
+
13
+ __author__ = "Capacium"
@@ -0,0 +1,4 @@
1
+ from .adapter import CapabilityAdapter
2
+ from .registry import AdaptationRegistry
3
+
4
+ __all__ = ["CapabilityAdapter", "AdaptationRegistry"]
@@ -0,0 +1,121 @@
1
+ """Capability Adapter — transforms capabilities between framework formats.
2
+
3
+ Uses exporters for format conversion and the adaptation registry
4
+ for target-specific configuration.
5
+ """
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from ..manifest import Manifest
9
+ from ..exporters import MCPExporter, A2AExporter
10
+ from .registry import AdaptationRegistry, AdaptationTarget
11
+
12
+
13
+ class AdaptationError(Exception):
14
+ """Raised when adaptation fails."""
15
+ pass
16
+
17
+
18
+ class CapabilityAdapter:
19
+ """Adapts capabilities to target framework formats."""
20
+
21
+ def __init__(self):
22
+ self._registry = AdaptationRegistry()
23
+ self._exporters = {
24
+ "mcp-server": MCPExporter(),
25
+ "a2a-agent": A2AExporter(),
26
+ }
27
+
28
+ @property
29
+ def registry(self) -> AdaptationRegistry:
30
+ return self._registry
31
+
32
+ def adapt(
33
+ self,
34
+ manifest: Manifest,
35
+ target: str,
36
+ options: Optional[Dict[str, Any]] = None,
37
+ ) -> Dict[str, Any]:
38
+ """Adapt a manifest to target framework format.
39
+
40
+ Args:
41
+ manifest: Source capability manifest
42
+ target: Target framework name (e.g. "mcp-server", "a2a-agent")
43
+ options: Optional adaptation options (transport, etc.)
44
+
45
+ Returns:
46
+ Adapted output as dict
47
+
48
+ Raises:
49
+ AdaptationError: If adaptation not possible
50
+ """
51
+ target_info = self._registry.get(target)
52
+ if target_info is None:
53
+ available = ", ".join(self._registry.list_targets())
54
+ raise AdaptationError(
55
+ f"Unknown adaptation target '{target}'. Available: {available}"
56
+ )
57
+
58
+ # Check if we have an exporter for this target
59
+ exporter = self._exporters.get(target)
60
+ if exporter is not None:
61
+ if not exporter.can_export(manifest):
62
+ raise AdaptationError(
63
+ f"Cannot adapt manifest kind '{manifest.kind}' to '{target}'"
64
+ )
65
+ result = exporter.export(manifest)
66
+ else:
67
+ # Generic adaptation via target info
68
+ result = self._generic_adapt(manifest, target_info)
69
+
70
+ # Apply target-specific options
71
+ if options:
72
+ result = self._apply_options(result, target_info, options)
73
+
74
+ return result
75
+
76
+ def can_adapt(self, manifest: Manifest, target: str) -> bool:
77
+ """Check if a manifest can be adapted to the target."""
78
+ target_info = self._registry.get(target)
79
+ if target_info is None:
80
+ return False
81
+ exporter = self._exporters.get(target)
82
+ if exporter is not None:
83
+ return exporter.can_export(manifest)
84
+ return True # Generic adaptation always possible
85
+
86
+ def list_targets(self, manifest: Optional[Manifest] = None) -> List[str]:
87
+ """List available adaptation targets, optionally filtered by manifest compatibility."""
88
+ if manifest is None:
89
+ return self._registry.list_targets()
90
+ return [t for t in self._registry.list_targets() if self.can_adapt(manifest, t)]
91
+
92
+ def _generic_adapt(self, manifest: Manifest, target: AdaptationTarget) -> Dict[str, Any]:
93
+ """Generic adaptation when no specific exporter exists."""
94
+ result: Dict[str, Any] = {
95
+ "name": manifest.name,
96
+ "version": manifest.version,
97
+ "description": manifest.description,
98
+ "kind": manifest.kind,
99
+ "adapted_from": "capacium",
100
+ "target": target.name,
101
+ }
102
+ if target.supports_tools and manifest.capabilities:
103
+ result["tools"] = [
104
+ {"name": c.get("name", ""), "description": c.get("description", "")}
105
+ for c in manifest.capabilities
106
+ ]
107
+ if manifest.runtimes:
108
+ result["runtime"] = manifest.runtimes
109
+ return result
110
+
111
+ def _apply_options(
112
+ self, result: Dict[str, Any], target: AdaptationTarget, options: Dict[str, Any]
113
+ ) -> Dict[str, Any]:
114
+ """Apply adaptation options to result."""
115
+ if target.requires_transport and "transport" in options:
116
+ result["transport"] = options["transport"]
117
+ if "command" in options:
118
+ result["command"] = options["command"]
119
+ if "args" in options:
120
+ result["args"] = options["args"]
121
+ return result
@@ -0,0 +1,58 @@
1
+ """Registry of supported adaptation targets.
2
+
3
+ Each target describes a framework + what kind of output it needs.
4
+ """
5
+ from dataclasses import dataclass
6
+ from typing import Dict, List, Optional
7
+
8
+
9
+ @dataclass
10
+ class AdaptationTarget:
11
+ """Describes a target framework for adaptation."""
12
+ name: str # e.g. "mcp-server", "a2a-agent", "claude-desktop"
13
+ description: str = ""
14
+ output_format: str = "json" # json, yaml, toml
15
+ requires_transport: bool = False # needs transport config (e.g. MCP)
16
+ supports_tools: bool = True
17
+ supports_resources: bool = True
18
+ supports_prompts: bool = False
19
+
20
+
21
+ class AdaptationRegistry:
22
+ """Registry of known adaptation targets."""
23
+
24
+ def __init__(self):
25
+ self._targets: Dict[str, AdaptationTarget] = {}
26
+ self._register_defaults()
27
+
28
+ def _register_defaults(self):
29
+ """Register built-in adaptation targets."""
30
+ self.register(AdaptationTarget(
31
+ name="mcp-server",
32
+ description="Model Context Protocol server descriptor",
33
+ requires_transport=True,
34
+ supports_prompts=True,
35
+ ))
36
+ self.register(AdaptationTarget(
37
+ name="a2a-agent",
38
+ description="Google A2A agent card",
39
+ supports_prompts=False,
40
+ ))
41
+ self.register(AdaptationTarget(
42
+ name="claude-desktop",
43
+ description="Claude Desktop MCP config entry",
44
+ output_format="json",
45
+ requires_transport=True,
46
+ ))
47
+
48
+ def register(self, target: AdaptationTarget) -> None:
49
+ self._targets[target.name] = target
50
+
51
+ def get(self, name: str) -> Optional[AdaptationTarget]:
52
+ return self._targets.get(name)
53
+
54
+ def list_targets(self) -> List[str]:
55
+ return list(self._targets.keys())
56
+
57
+ def all(self) -> List[AdaptationTarget]:
58
+ return list(self._targets.values())
@@ -0,0 +1,124 @@
1
+ from typing import List
2
+ from .base import FrameworkAdapter
3
+ from .opencode import OpenCodeAdapter, OpencodeCommandAdapter
4
+ from .claude_code import ClaudeCodeAdapter
5
+ from .gemini_cli import GeminiCLIAdapter
6
+ from .cursor import CursorAdapter
7
+ from .continue_dev import ContinueDevAdapter
8
+ from .claude_desktop import ClaudeDesktopAdapter
9
+ from .windsurf import WindsurfAdapter
10
+ from .cline import ClineAdapter
11
+ from .zed import ZedAdapter
12
+ from .codex import CodexAdapter
13
+ from .antigravity import AntigravityAdapter
14
+ from .sourcegraph_cody import SourcegraphCodyAdapter
15
+ from .librechat import LibreChatAdapter
16
+ from .chainlit import ChainlitAdapter
17
+ from .cherry_studio import CherryStudioAdapter
18
+ from .nextchat import NextChatAdapter
19
+ from .desktop_commander import DesktopCommanderAdapter
20
+ from .junie import JunieAdapter
21
+ from .stub_adapters import LutraAdapter, SergeAdapter, NotebookLMAdapter, McpRemoteAdapter
22
+ from .roo_code import RooCodeAdapter
23
+ from .goose import GooseAdapter
24
+ from .aider import AiderAdapter
25
+ from .openclaw import OpenClawAdapter
26
+ from .hermes import HermesAdapter
27
+ from .qwen import QwenAdapter
28
+ from .copilot import CopilotAdapter
29
+ from .langchain_bridge import LangChainToolAdapter, FlowiseAdapter
30
+
31
+ _ADAPTER_REGISTRY: dict[str, type[FrameworkAdapter]] = {}
32
+
33
+
34
+ def register_adapter(name: str, adapter_cls: type[FrameworkAdapter]) -> None:
35
+ _ADAPTER_REGISTRY[name] = adapter_cls
36
+
37
+
38
+ def get_adapter(name: str) -> FrameworkAdapter:
39
+ cls = _ADAPTER_REGISTRY.get(name)
40
+ if cls is None:
41
+ raise ValueError(f"Unknown framework adapter: {name}")
42
+ return cls()
43
+
44
+
45
+ def get_adapter_for_manifest(manifest) -> FrameworkAdapter:
46
+ frameworks = (
47
+ manifest.get_target_frameworks()
48
+ if hasattr(manifest, "get_target_frameworks")
49
+ else getattr(manifest, "frameworks", None) or []
50
+ )
51
+ if not frameworks:
52
+ return get_adapter("opencode")
53
+ for fw in frameworks:
54
+ if fw in _ADAPTER_REGISTRY:
55
+ return get_adapter(fw)
56
+ return get_adapter("opencode")
57
+
58
+
59
+ def get_adapters_for_manifest(manifest) -> List[FrameworkAdapter]:
60
+ frameworks = (
61
+ manifest.get_target_frameworks()
62
+ if hasattr(manifest, "get_target_frameworks")
63
+ else getattr(manifest, "frameworks", None) or []
64
+ )
65
+ if not frameworks:
66
+ return [get_adapter("opencode")]
67
+ adapters = []
68
+ for fw in frameworks:
69
+ fw_clean = fw.strip()
70
+ if fw_clean in _ADAPTER_REGISTRY:
71
+ adapters.append(get_adapter(fw_clean))
72
+ if not adapters:
73
+ adapters.append(get_adapter("opencode"))
74
+ return adapters
75
+
76
+
77
+ def list_registered_adapters() -> List[str]:
78
+ """Return sorted list of all registered adapter names."""
79
+ return sorted(_ADAPTER_REGISTRY.keys())
80
+
81
+
82
+ # ── Tier 1: Development & Engineering ──────────────────────────────────
83
+ register_adapter("opencode", OpenCodeAdapter)
84
+ register_adapter("opencode-command", OpencodeCommandAdapter)
85
+ register_adapter("claude-desktop", ClaudeDesktopAdapter)
86
+ register_adapter("claude-code", ClaudeCodeAdapter)
87
+ register_adapter("cursor", CursorAdapter)
88
+ register_adapter("windsurf", WindsurfAdapter)
89
+ register_adapter("cline", ClineAdapter)
90
+ register_adapter("zed", ZedAdapter)
91
+ register_adapter("sourcegraph-cody", SourcegraphCodyAdapter)
92
+ register_adapter("antigravity", AntigravityAdapter)
93
+ register_adapter("continue-dev", ContinueDevAdapter)
94
+ register_adapter("codex", CodexAdapter)
95
+ register_adapter("qwen", QwenAdapter)
96
+ register_adapter("gemini-cli", GeminiCLIAdapter)
97
+
98
+ # ── Tier 2: Specialized / Workflow ─────────────────────────────────────
99
+ register_adapter("librechat", LibreChatAdapter)
100
+ register_adapter("chainlit", ChainlitAdapter)
101
+ register_adapter("cherry-studio", CherryStudioAdapter)
102
+ register_adapter("nextchat", NextChatAdapter)
103
+ register_adapter("desktop-commander", DesktopCommanderAdapter)
104
+ register_adapter("notebooklm", NotebookLMAdapter)
105
+ register_adapter("lutra", LutraAdapter)
106
+ register_adapter("serge", SergeAdapter)
107
+ register_adapter("mcp-remote", McpRemoteAdapter)
108
+
109
+ # ── Tier 3: Extended Skill Clients ─────────────────────────────────────
110
+ register_adapter("roo-code", RooCodeAdapter)
111
+ register_adapter("goose", GooseAdapter)
112
+ register_adapter("aider", AiderAdapter)
113
+ register_adapter("openclaw", OpenClawAdapter)
114
+
115
+ # ── Tier 3c: Junie ──────────────────────────────────────────────────────
116
+ register_adapter("junie", JunieAdapter)
117
+
118
+ # ── Tier 4: Agent Flow Bridging ────────────────────────────────────────
119
+ register_adapter("langchain", LangChainToolAdapter)
120
+ register_adapter("flowise", FlowiseAdapter)
121
+
122
+ # ── Tier 5: SKILL.md-Capable Agents (2026 standard) ────────────────────
123
+ register_adapter("hermes", HermesAdapter)
124
+ register_adapter("copilot", CopilotAdapter)
@@ -0,0 +1,91 @@
1
+ """Aider AI coding assistant MCP adapter.
2
+
3
+ Config: ~/.aider.conf.yml or project-level .aider.conf.yml
4
+ Aider can connect to MCP servers via its configuration YAML.
5
+ """
6
+ from pathlib import Path
7
+
8
+ from ..storage import StorageManager
9
+ from .base import FrameworkAdapter, ensure_package_dir
10
+ from .mcp_config_patcher import McpConfigPatcher
11
+
12
+
13
+ class AiderAdapter(FrameworkAdapter):
14
+
15
+ def __init__(self):
16
+ self.storage = StorageManager()
17
+ self.config_path = Path.home() / ".aider.conf.yml"
18
+
19
+ def install_skill(self, cap_name: str, version: str, source_dir: Path, owner: str = "global") -> bool:
20
+ print("Aider does not support skill symlinking. Use 'mcp-server' kind.")
21
+ return False
22
+
23
+ def remove_skill(self, cap_name: str, owner: str = "global") -> bool:
24
+ return False
25
+
26
+ def install_mcp_server(self, cap_name: str, version: str, source_dir: Path, owner: str = "global") -> bool:
27
+ package_dir = ensure_package_dir(self.storage, cap_name, version, source_dir, owner=owner)
28
+
29
+ from ..manifest import Manifest
30
+ manifest = Manifest.detect_from_directory(package_dir)
31
+ mcp_meta = manifest.get_mcp_metadata()
32
+ entry = McpConfigPatcher.build_mcp_entry(cap_name, package_dir, mcp_meta)
33
+
34
+ McpConfigPatcher.backup(self.config_path)
35
+
36
+ config: dict = {}
37
+ if self.config_path.exists():
38
+ try:
39
+ import yaml
40
+ with open(self.config_path) as f:
41
+ config = yaml.safe_load(f) or {}
42
+ except ImportError:
43
+ config = {}
44
+
45
+ servers = config.setdefault("mcp-servers", {})
46
+ servers[cap_name] = entry
47
+
48
+ self.config_path.parent.mkdir(parents=True, exist_ok=True)
49
+ try:
50
+ import yaml
51
+ with open(self.config_path, "w") as f:
52
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
53
+ except ImportError:
54
+ # Minimal YAML fallback
55
+ with open(self.config_path, "w") as f:
56
+ f.write("mcp-servers:\n")
57
+ for sname, sentry in servers.items():
58
+ f.write(f" {sname}:\n")
59
+ for k, v in sentry.items():
60
+ f.write(f" {k}: {v}\n")
61
+
62
+ return True
63
+
64
+ def remove_mcp_server(self, cap_name: str, owner: str = "global") -> bool:
65
+ if not self.config_path.exists():
66
+ return True
67
+
68
+ McpConfigPatcher.backup(self.config_path)
69
+ try:
70
+ import yaml
71
+ with open(self.config_path) as f:
72
+ config = yaml.safe_load(f) or {}
73
+ servers = config.get("mcp-servers", {})
74
+ if cap_name in servers:
75
+ del servers[cap_name]
76
+ with open(self.config_path, "w") as f:
77
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
78
+ except ImportError:
79
+ pass
80
+ return True
81
+
82
+ def capability_exists(self, cap_name: str) -> bool:
83
+ if not self.config_path.exists():
84
+ return False
85
+ try:
86
+ import yaml
87
+ with open(self.config_path) as f:
88
+ config = yaml.safe_load(f) or {}
89
+ return cap_name in config.get("mcp-servers", {})
90
+ except ImportError:
91
+ return False
@@ -0,0 +1,78 @@
1
+ """Google Antigravity adapter.
2
+
3
+ Skills: ~/.gemini/config/skills/<name>/SKILL.md
4
+ MCP: ~/.gemini/config/mcp_config.json
5
+ """
6
+ import json
7
+ import shutil
8
+ from pathlib import Path
9
+
10
+ from ..storage import StorageManager
11
+ from ..symlink_manager import SymlinkManager
12
+ from .base import FrameworkAdapter, _cap_id, ensure_package_dir
13
+ from .mcp_config_patcher import McpConfigPatcher
14
+
15
+
16
+ class AntigravityAdapter(FrameworkAdapter):
17
+
18
+ def __init__(self):
19
+ self.storage = StorageManager()
20
+ self.symlink_manager = SymlinkManager()
21
+ self.config_path = Path.home() / ".gemini" / "config" / "mcp_config.json"
22
+ self.skills_dir = Path.home() / ".gemini" / "config" / "skills"
23
+ self.legacy_skills_dir = Path.home() / ".gemini" / "antigravity" / "skills"
24
+ self.skills_dir.mkdir(parents=True, exist_ok=True)
25
+
26
+ def install_skill(self, cap_name: str, version: str, source_dir: Path, owner: str = "global") -> bool:
27
+ package_dir = ensure_package_dir(self.storage, cap_name, version, source_dir, owner=owner)
28
+
29
+ link_path = self.skills_dir / _cap_id(cap_name, owner)
30
+ success = self.symlink_manager.create_symlink(package_dir, link_path)
31
+
32
+ metadata_path = package_dir / ".capacium-meta.json"
33
+ with open(metadata_path, "w") as f:
34
+ json.dump({"name": cap_name, "version": version, "owner": owner}, f, indent=2)
35
+
36
+ return success
37
+
38
+ def remove_skill(self, cap_name: str, owner: str = "global") -> bool:
39
+ for skills_dir in (self.skills_dir, self.legacy_skills_dir):
40
+ link_path = skills_dir / _cap_id(cap_name, owner)
41
+ if link_path.exists() or link_path.is_symlink():
42
+ if link_path.is_symlink():
43
+ self.symlink_manager.remove_symlink(link_path)
44
+ elif link_path.is_dir():
45
+ shutil.rmtree(link_path)
46
+ else:
47
+ link_path.unlink()
48
+ return True
49
+
50
+ def install_mcp_server(self, cap_name: str, version: str, source_dir: Path, owner: str = "global") -> bool:
51
+ package_dir = ensure_package_dir(self.storage, cap_name, version, source_dir, owner=owner)
52
+
53
+ from ..manifest import Manifest
54
+ manifest = Manifest.detect_from_directory(package_dir)
55
+ mcp_meta = manifest.get_mcp_metadata()
56
+
57
+ return McpConfigPatcher.inject_json_mcp_server(
58
+ config_path=self.config_path,
59
+ server_key=McpConfigPatcher.build_server_key(cap_name, owner),
60
+ mcp_section_key="mcpServers",
61
+ cap_name=cap_name,
62
+ source_dir=package_dir,
63
+ mcp_meta=mcp_meta,
64
+ )
65
+
66
+ def remove_mcp_server(self, cap_name: str, owner: str = "global") -> bool:
67
+ return McpConfigPatcher.remove_json_mcp_server(
68
+ self.config_path, McpConfigPatcher.build_server_key(cap_name, owner), "mcpServers",
69
+ )
70
+
71
+ def capability_exists(self, cap_name: str, owner: str = "global") -> bool:
72
+ for skills_dir in (self.skills_dir, self.legacy_skills_dir):
73
+ link_path = skills_dir / _cap_id(cap_name, owner)
74
+ if link_path.exists() and link_path.is_symlink():
75
+ return True
76
+ return McpConfigPatcher.mcp_server_exists_json(
77
+ self.config_path, McpConfigPatcher.build_server_key(cap_name, owner), "mcpServers",
78
+ )
@@ -0,0 +1,61 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+
4
+
5
+ def _cap_id(cap_name: str, owner: str = "global") -> str:
6
+ """Return the client-facing filesystem name for a capability.
7
+
8
+ Skill clients discover direct children of their skills directory, so owner
9
+ namespaces cannot be represented as nested directories here. Cross-owner
10
+ conflicts are handled before adapter installation.
11
+ """
12
+ return cap_name
13
+
14
+
15
+ def ensure_package_dir(storage, cap_name: str, version: str, source_dir: Path, owner: str = "global") -> Path:
16
+ """Copy source_dir → package_dir if they differ. Returns package_dir."""
17
+ package_dir = storage.get_package_dir(cap_name, version, owner=owner)
18
+ if source_dir.resolve() != package_dir.resolve():
19
+ import shutil
20
+ if package_dir.exists():
21
+ shutil.rmtree(package_dir)
22
+ shutil.copytree(source_dir, package_dir)
23
+ return package_dir
24
+
25
+
26
+ class FrameworkAdapter(ABC):
27
+ def install_capability(self, cap_name: str, version: str, source_dir: Path, owner: str = "global", kind: str = "skill") -> bool:
28
+ if kind == "mcp-server":
29
+ return self.install_mcp_server(cap_name, version, source_dir, owner)
30
+ from ..models import SKILL_LAYER_KIND_VALUES
31
+ if kind not in SKILL_LAYER_KIND_VALUES:
32
+ # Kind-placement contract (V6): bundle/connector-pack roots are
33
+ # containers — their members get placed individually; the root
34
+ # itself produces no client artifacts.
35
+ return True
36
+ return self.install_skill(cap_name, version, source_dir, owner)
37
+
38
+ def remove_capability(self, cap_name: str, owner: str = "global", kind: str = "skill") -> bool:
39
+ if kind == "mcp-server":
40
+ return self.remove_mcp_server(cap_name, owner)
41
+ return self.remove_skill(cap_name, owner)
42
+
43
+ @abstractmethod
44
+ def install_skill(self, cap_name: str, version: str, source_dir: Path, owner: str = "global") -> bool:
45
+ ...
46
+
47
+ @abstractmethod
48
+ def remove_skill(self, cap_name: str, owner: str = "global") -> bool:
49
+ ...
50
+
51
+ @abstractmethod
52
+ def install_mcp_server(self, cap_name: str, version: str, source_dir: Path, owner: str = "global") -> bool:
53
+ ...
54
+
55
+ @abstractmethod
56
+ def remove_mcp_server(self, cap_name: str, owner: str = "global") -> bool:
57
+ ...
58
+
59
+ @abstractmethod
60
+ def capability_exists(self, cap_name: str) -> bool:
61
+ ...