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.
- capacium/__init__.py +13 -0
- capacium/adaptation/__init__.py +4 -0
- capacium/adaptation/adapter.py +121 -0
- capacium/adaptation/registry.py +58 -0
- capacium/adapters/__init__.py +124 -0
- capacium/adapters/aider.py +91 -0
- capacium/adapters/antigravity.py +78 -0
- capacium/adapters/base.py +61 -0
- capacium/adapters/capability_adapter.py +310 -0
- capacium/adapters/chainlit.py +49 -0
- capacium/adapters/cherry_studio.py +49 -0
- capacium/adapters/claude_code.py +112 -0
- capacium/adapters/claude_desktop.py +147 -0
- capacium/adapters/cline.py +51 -0
- capacium/adapters/codex.py +111 -0
- capacium/adapters/continue_dev.py +134 -0
- capacium/adapters/copilot.py +96 -0
- capacium/adapters/cursor.py +152 -0
- capacium/adapters/desktop_commander.py +49 -0
- capacium/adapters/gemini_cli.py +122 -0
- capacium/adapters/goose.py +92 -0
- capacium/adapters/hermes.py +77 -0
- capacium/adapters/junie.py +109 -0
- capacium/adapters/langchain_bridge.py +149 -0
- capacium/adapters/librechat.py +51 -0
- capacium/adapters/mcp_config_patcher.py +562 -0
- capacium/adapters/nextchat.py +49 -0
- capacium/adapters/openclaw.py +77 -0
- capacium/adapters/opencode.py +218 -0
- capacium/adapters/qwen.py +77 -0
- capacium/adapters/roo_code.py +50 -0
- capacium/adapters/sourcegraph_cody.py +49 -0
- capacium/adapters/stub_adapters.py +77 -0
- capacium/adapters/windsurf.py +49 -0
- capacium/adapters/zed.py +49 -0
- capacium/cli.py +977 -0
- capacium/commands/__init__.py +0 -0
- capacium/commands/_resolve.py +142 -0
- capacium/commands/adapt.py +103 -0
- capacium/commands/block_status.py +73 -0
- capacium/commands/browse.py +762 -0
- capacium/commands/compare.py +347 -0
- capacium/commands/doctor.py +527 -0
- capacium/commands/export.py +119 -0
- capacium/commands/hold.py +112 -0
- capacium/commands/info.py +408 -0
- capacium/commands/init.py +547 -0
- capacium/commands/install.py +1613 -0
- capacium/commands/key.py +57 -0
- capacium/commands/license.py +123 -0
- capacium/commands/list_capabilities.py +135 -0
- capacium/commands/lock.py +115 -0
- capacium/commands/marketplace.py +45 -0
- capacium/commands/package.py +115 -0
- capacium/commands/policy.py +214 -0
- capacium/commands/publish.py +241 -0
- capacium/commands/registry.py +98 -0
- capacium/commands/registry_cmd.py +175 -0
- capacium/commands/remove.py +359 -0
- capacium/commands/repair.py +476 -0
- capacium/commands/runtimes_cmd.py +61 -0
- capacium/commands/sandbox.py +129 -0
- capacium/commands/search.py +634 -0
- capacium/commands/sign.py +85 -0
- capacium/commands/skills_mcp.py +235 -0
- capacium/commands/submit.py +110 -0
- capacium/commands/update.py +293 -0
- capacium/commands/validate.py +324 -0
- capacium/commands/verify.py +153 -0
- capacium/conditions.py +265 -0
- capacium/exporters/__init__.py +4 -0
- capacium/exporters/a2a_exporter.py +57 -0
- capacium/exporters/base.py +31 -0
- capacium/exporters/mcp_exporter.py +52 -0
- capacium/fingerprint.py +54 -0
- capacium/framework_detector.py +262 -0
- capacium/index.py +420 -0
- capacium/manifest.py +331 -0
- capacium/migrations/__init__.py +1 -0
- capacium/models.py +199 -0
- capacium/registry.py +542 -0
- capacium/registry_client.py +476 -0
- capacium/runtimes.py +525 -0
- capacium/signing.py +302 -0
- capacium/skills_mcp_wrapper.py +310 -0
- capacium/storage.py +171 -0
- capacium/symlink_manager.py +74 -0
- capacium/sync.py +170 -0
- capacium/taxonomy.py +237 -0
- capacium/telemetry.py +105 -0
- capacium/tui_hints.py +189 -0
- capacium/ui.py +346 -0
- capacium/utils/__init__.py +0 -0
- capacium/utils/config.py +205 -0
- capacium/utils/copytree.py +73 -0
- capacium/utils/errors.py +22 -0
- capacium/utils/mcp_probe.py +91 -0
- capacium/utils/project_scope.py +25 -0
- capacium/utils/table.py +150 -0
- capacium/utils/toml_compat.py +6 -0
- capacium/versioning.py +136 -0
- capacium-0.15.0.dist-info/METADATA +224 -0
- capacium-0.15.0.dist-info/RECORD +107 -0
- capacium-0.15.0.dist-info/WHEEL +5 -0
- capacium-0.15.0.dist-info/entry_points.txt +3 -0
- capacium-0.15.0.dist-info/licenses/LICENSE +201 -0
- 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,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
|
+
...
|