aiaddons 0.1.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.
- aiaddons/__init__.py +5 -0
- aiaddons/__main__.py +6 -0
- aiaddons/agents/__init__.py +16 -0
- aiaddons/agents/antigravity.py +106 -0
- aiaddons/agents/base.py +31 -0
- aiaddons/agents/claude_code.py +85 -0
- aiaddons/agents/codex.py +81 -0
- aiaddons/agents/manager.py +58 -0
- aiaddons/agents/utils.py +48 -0
- aiaddons/cli/__init__.py +5 -0
- aiaddons/cli/commands/__init__.py +1 -0
- aiaddons/cli/commands/agents.py +74 -0
- aiaddons/cli/commands/compatibility.py +110 -0
- aiaddons/cli/commands/doctor.py +122 -0
- aiaddons/cli/commands/install.py +963 -0
- aiaddons/cli/commands/registry.py +345 -0
- aiaddons/cli/commands/remove.py +564 -0
- aiaddons/cli/commands/sync.py +599 -0
- aiaddons/cli/commands/tui.py +20 -0
- aiaddons/cli/commands/update.py +667 -0
- aiaddons/cli/exit_codes.py +17 -0
- aiaddons/cli/main.py +87 -0
- aiaddons/core/__init__.py +1 -0
- aiaddons/core/acquisition/__init__.py +29 -0
- aiaddons/core/acquisition/base.py +38 -0
- aiaddons/core/acquisition/engine.py +97 -0
- aiaddons/core/acquisition/extractor.py +268 -0
- aiaddons/core/acquisition/git.py +163 -0
- aiaddons/core/acquisition/local.py +136 -0
- aiaddons/core/acquisition/models.py +42 -0
- aiaddons/core/acquisition/package.py +94 -0
- aiaddons/core/acquisition/staging.py +97 -0
- aiaddons/core/acquisition/url.py +162 -0
- aiaddons/core/compatibility/__init__.py +15 -0
- aiaddons/core/compatibility/engine.py +365 -0
- aiaddons/core/compatibility/models.py +49 -0
- aiaddons/core/drift.py +73 -0
- aiaddons/core/exceptions.py +119 -0
- aiaddons/core/execution/__init__.py +37 -0
- aiaddons/core/execution/engine.py +1730 -0
- aiaddons/core/execution/external/__init__.py +27 -0
- aiaddons/core/execution/external/models.py +47 -0
- aiaddons/core/execution/external/runner.py +165 -0
- aiaddons/core/execution/external/runtimes.py +136 -0
- aiaddons/core/execution/external/security.py +115 -0
- aiaddons/core/execution/models.py +44 -0
- aiaddons/core/execution/primitives.py +213 -0
- aiaddons/core/execution/security.py +82 -0
- aiaddons/core/health/__init__.py +17 -0
- aiaddons/core/health/engine.py +1144 -0
- aiaddons/core/health/models.py +75 -0
- aiaddons/core/installer/__init__.py +47 -0
- aiaddons/core/installer/engine.py +515 -0
- aiaddons/core/installer/models.py +596 -0
- aiaddons/core/models/__init__.py +47 -0
- aiaddons/core/models/agent.py +63 -0
- aiaddons/core/models/manifest.py +448 -0
- aiaddons/core/models/stack.py +117 -0
- aiaddons/core/secrets/__init__.py +29 -0
- aiaddons/core/secrets/models.py +52 -0
- aiaddons/core/secrets/resolver.py +472 -0
- aiaddons/core/sync/__init__.py +21 -0
- aiaddons/core/sync/engine.py +601 -0
- aiaddons/core/sync/models.py +106 -0
- aiaddons/core/update/__init__.py +6 -0
- aiaddons/core/update/engine.py +501 -0
- aiaddons/core/update/models.py +78 -0
- aiaddons/core/verification/__init__.py +16 -0
- aiaddons/core/verification/engine.py +916 -0
- aiaddons/core/verification/models.py +51 -0
- aiaddons/integrations/__init__.py +15 -0
- aiaddons/integrations/base.py +38 -0
- aiaddons/integrations/cli_tool.py +97 -0
- aiaddons/integrations/mcp.py +190 -0
- aiaddons/integrations/plugin.py +241 -0
- aiaddons/integrations/skill.py +128 -0
- aiaddons/registry/__init__.py +22 -0
- aiaddons/registry/addons/brave-search-mcp.yaml +48 -0
- aiaddons/registry/addons/caveman.yaml +35 -0
- aiaddons/registry/addons/context7-mcp.yaml +48 -0
- aiaddons/registry/addons/figma-mcp.yaml +50 -0
- aiaddons/registry/addons/filesystem-mcp.yaml +40 -0
- aiaddons/registry/addons/firecrawl-mcp.yaml +50 -0
- aiaddons/registry/addons/github-mcp.yaml +48 -0
- aiaddons/registry/addons/karpathy-behavioral-skill.yaml +36 -0
- aiaddons/registry/addons/notion-mcp.yaml +49 -0
- aiaddons/registry/addons/playwright-mcp.yaml +41 -0
- aiaddons/registry/addons/ponytail-audit-skill.yaml +32 -0
- aiaddons/registry/addons/ponytail-debt-skill.yaml +32 -0
- aiaddons/registry/addons/ponytail-gain-skill.yaml +32 -0
- aiaddons/registry/addons/ponytail-help-skill.yaml +32 -0
- aiaddons/registry/addons/ponytail-review-skill.yaml +32 -0
- aiaddons/registry/addons/ponytail-skill.yaml +32 -0
- aiaddons/registry/addons/ponytail.yaml +40 -0
- aiaddons/registry/addons/postgres-mcp.yaml +47 -0
- aiaddons/registry/addons/python-lint-plugin.yaml +38 -0
- aiaddons/registry/addons/sentry-mcp.yaml +48 -0
- aiaddons/registry/addons/sequential-thinking-mcp.yaml +40 -0
- aiaddons/registry/addons/skill-creator.yaml +37 -0
- aiaddons/registry/addons/stripe-mcp.yaml +48 -0
- aiaddons/registry/addons/supabase-mcp.yaml +48 -0
- aiaddons/registry/addons/superpowers-brainstorming.yaml +35 -0
- aiaddons/registry/addons/superpowers-planning.yaml +35 -0
- aiaddons/registry/addons/superpowers-review.yaml +35 -0
- aiaddons/registry/addons/superpowers-tdd.yaml +35 -0
- aiaddons/registry/addons/superpowers-worktree.yaml +40 -0
- aiaddons/registry/addons/superpowers.yaml +40 -0
- aiaddons/registry/addons/vibesec-skill.yaml +35 -0
- aiaddons/registry/cache.py +126 -0
- aiaddons/registry/client.py +85 -0
- aiaddons/registry/loader.py +103 -0
- aiaddons/registry/models.py +45 -0
- aiaddons/registry/registry.py +179 -0
- aiaddons/registry/validator.py +123 -0
- aiaddons/state/__init__.py +21 -0
- aiaddons/state/lockfile.py +136 -0
- aiaddons/state/locking.py +55 -0
- aiaddons/state/store.py +158 -0
- aiaddons/state/transaction.py +136 -0
- aiaddons/tui/__init__.py +5 -0
- aiaddons/tui/app.py +1010 -0
- aiaddons/tui/screens/__init__.py +17 -0
- aiaddons/tui/screens/health.py +266 -0
- aiaddons/tui/screens/modals.py +314 -0
- aiaddons/tui/screens/sync.py +387 -0
- aiaddons-0.1.0.dist-info/METADATA +458 -0
- aiaddons-0.1.0.dist-info/RECORD +130 -0
- aiaddons-0.1.0.dist-info/WHEEL +4 -0
- aiaddons-0.1.0.dist-info/entry_points.txt +2 -0
- aiaddons-0.1.0.dist-info/licenses/LICENSE +21 -0
aiaddons/__init__.py
ADDED
aiaddons/__main__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Agent detection and adapters subpackage."""
|
|
2
|
+
|
|
3
|
+
from aiaddons.agents.antigravity import AntigravityAdapter
|
|
4
|
+
from aiaddons.agents.base import BaseAgentAdapter
|
|
5
|
+
from aiaddons.agents.claude_code import ClaudeCodeAdapter
|
|
6
|
+
from aiaddons.agents.codex import CodexAdapter
|
|
7
|
+
from aiaddons.agents.manager import AgentDetectionManager, detect_agents
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AntigravityAdapter",
|
|
11
|
+
"BaseAgentAdapter",
|
|
12
|
+
"ClaudeCodeAdapter",
|
|
13
|
+
"CodexAdapter",
|
|
14
|
+
"AgentDetectionManager",
|
|
15
|
+
"detect_agents",
|
|
16
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Antigravity CLI agent detection adapter."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from aiaddons.agents.utils import find_executable, run_version_command
|
|
6
|
+
from aiaddons.core.models.agent import AgentCapability, AgentDetectionResult, Scope
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AntigravityAdapter:
|
|
10
|
+
"""Adapter for detecting and configuring Antigravity CLI."""
|
|
11
|
+
|
|
12
|
+
agent_id: str = "antigravity"
|
|
13
|
+
name: str = "Antigravity CLI"
|
|
14
|
+
capabilities: list[AgentCapability] = [AgentCapability.MCP, AgentCapability.SKILL]
|
|
15
|
+
|
|
16
|
+
def supports_capability(self, capability: AgentCapability) -> bool:
|
|
17
|
+
"""Check if Antigravity CLI supports the given capability."""
|
|
18
|
+
return capability in self.capabilities
|
|
19
|
+
|
|
20
|
+
def get_config_path(self, scope: Scope, project_path: Path | None = None) -> Path | None:
|
|
21
|
+
"""Get the configuration path for Antigravity CLI based on scope."""
|
|
22
|
+
if scope == Scope.GLOBAL:
|
|
23
|
+
global_mcp = Path.home() / ".gemini" / "config" / "mcp_config.json"
|
|
24
|
+
if global_mcp.exists():
|
|
25
|
+
return global_mcp
|
|
26
|
+
global_config_dir = Path.home() / ".gemini" / "config"
|
|
27
|
+
if global_config_dir.exists():
|
|
28
|
+
return global_mcp
|
|
29
|
+
global_gemini_dir = Path.home() / ".gemini"
|
|
30
|
+
if global_gemini_dir.exists():
|
|
31
|
+
return global_mcp
|
|
32
|
+
return global_mcp
|
|
33
|
+
|
|
34
|
+
base_dir = project_path if project_path is not None else Path.cwd()
|
|
35
|
+
ws_mcp = base_dir / ".agents" / "mcp_config.json"
|
|
36
|
+
if ws_mcp.exists():
|
|
37
|
+
return ws_mcp
|
|
38
|
+
ws_agents = base_dir / ".agents"
|
|
39
|
+
if ws_agents.exists():
|
|
40
|
+
return ws_mcp
|
|
41
|
+
return ws_mcp
|
|
42
|
+
|
|
43
|
+
def get_skill_directory(self, scope: Scope, project_path: Path | None = None) -> Path:
|
|
44
|
+
"""Get the skill installation directory for Antigravity CLI based on scope."""
|
|
45
|
+
if scope == Scope.GLOBAL:
|
|
46
|
+
return Path.home() / ".gemini" / "config" / "skills"
|
|
47
|
+
base_dir = project_path if project_path is not None else Path.cwd()
|
|
48
|
+
return base_dir / ".agents" / "skills"
|
|
49
|
+
|
|
50
|
+
def detect(self, project_path: Path | None = None) -> AgentDetectionResult:
|
|
51
|
+
"""Detect Antigravity CLI installation status, binary path, version, and config location."""
|
|
52
|
+
exec_path = find_executable("agy")
|
|
53
|
+
if exec_path is None:
|
|
54
|
+
exec_path = find_executable("antigravity")
|
|
55
|
+
|
|
56
|
+
global_config = self.get_config_path(Scope.GLOBAL)
|
|
57
|
+
workspace_config = self.get_config_path(Scope.WORKSPACE, project_path=project_path)
|
|
58
|
+
|
|
59
|
+
base_dir = project_path if project_path is not None else Path.cwd()
|
|
60
|
+
has_global = (
|
|
61
|
+
(global_config is not None and global_config.exists())
|
|
62
|
+
or (Path.home() / ".gemini").exists()
|
|
63
|
+
)
|
|
64
|
+
has_workspace = (
|
|
65
|
+
(workspace_config is not None and workspace_config.exists())
|
|
66
|
+
or (base_dir / ".agents").exists()
|
|
67
|
+
or (base_dir / "AGENTS.md").exists()
|
|
68
|
+
or (base_dir / "GEMINI.md").exists()
|
|
69
|
+
)
|
|
70
|
+
installed = (exec_path is not None) or has_global or has_workspace
|
|
71
|
+
|
|
72
|
+
version: str | None = None
|
|
73
|
+
detection_error: str | None = None
|
|
74
|
+
|
|
75
|
+
if exec_path is not None:
|
|
76
|
+
try:
|
|
77
|
+
cmd_name = exec_path.name
|
|
78
|
+
if cmd_name.lower().endswith(".exe"):
|
|
79
|
+
cmd_name = cmd_name[:-4]
|
|
80
|
+
version = run_version_command(cmd_name)
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
detection_error = f"Error running version command: {exc}"
|
|
83
|
+
|
|
84
|
+
active_config = (
|
|
85
|
+
workspace_config
|
|
86
|
+
if (has_workspace and workspace_config and workspace_config.exists())
|
|
87
|
+
else (
|
|
88
|
+
global_config
|
|
89
|
+
if (has_global and global_config and global_config.exists())
|
|
90
|
+
else (workspace_config or global_config)
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return AgentDetectionResult(
|
|
95
|
+
agent_id=self.agent_id,
|
|
96
|
+
name=self.name,
|
|
97
|
+
installed=installed,
|
|
98
|
+
version=version,
|
|
99
|
+
executable_path=str(exec_path) if exec_path else None,
|
|
100
|
+
global_config_path=str(global_config) if global_config else None,
|
|
101
|
+
workspace_config_path=str(workspace_config) if workspace_config else None,
|
|
102
|
+
config_path=str(active_config) if active_config else None,
|
|
103
|
+
supported_scopes=[Scope.GLOBAL, Scope.WORKSPACE],
|
|
104
|
+
capabilities=list(self.capabilities),
|
|
105
|
+
detection_error=detection_error,
|
|
106
|
+
)
|
aiaddons/agents/base.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Base agent adapter protocol defining standard interface for agent detection and configuration."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
from aiaddons.core.models.agent import AgentCapability, AgentDetectionResult, Scope
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@runtime_checkable
|
|
10
|
+
class BaseAgentAdapter(Protocol):
|
|
11
|
+
"""Protocol for AI coding agent detection and configuration adapters."""
|
|
12
|
+
|
|
13
|
+
agent_id: str
|
|
14
|
+
name: str
|
|
15
|
+
capabilities: list[AgentCapability]
|
|
16
|
+
|
|
17
|
+
def detect(self, project_path: Path | None = None) -> AgentDetectionResult:
|
|
18
|
+
"""Detect agent installation status, binary path, version, and config location."""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
def get_config_path(self, scope: Scope, project_path: Path | None = None) -> Path | None:
|
|
22
|
+
"""Get path to the agent's configuration file for the specified scope."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
def supports_capability(self, capability: AgentCapability) -> bool:
|
|
26
|
+
"""Check if this agent supports a specific integration capability."""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def get_skill_directory(self, scope: Scope, project_path: Path | None = None) -> Path:
|
|
30
|
+
"""Get canonical skill installation directory for the specified scope."""
|
|
31
|
+
...
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Claude Code agent detection adapter."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from aiaddons.agents.utils import find_executable, run_version_command
|
|
6
|
+
from aiaddons.core.models.agent import AgentCapability, AgentDetectionResult, Scope
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ClaudeCodeAdapter:
|
|
10
|
+
"""Adapter for detecting and configuring Claude Code."""
|
|
11
|
+
|
|
12
|
+
agent_id: str = "claude-code"
|
|
13
|
+
name: str = "Claude Code"
|
|
14
|
+
capabilities: list[AgentCapability] = [AgentCapability.MCP, AgentCapability.SKILL]
|
|
15
|
+
|
|
16
|
+
def supports_capability(self, capability: AgentCapability) -> bool:
|
|
17
|
+
"""Check if Claude Code supports the given capability."""
|
|
18
|
+
return capability in self.capabilities
|
|
19
|
+
|
|
20
|
+
def get_config_path(self, scope: Scope, project_path: Path | None = None) -> Path | None:
|
|
21
|
+
"""Get the configuration path for Claude Code based on scope."""
|
|
22
|
+
if scope == Scope.GLOBAL:
|
|
23
|
+
global_json = Path.home() / ".claude.json"
|
|
24
|
+
if global_json.exists():
|
|
25
|
+
return global_json
|
|
26
|
+
global_dir = Path.home() / ".claude"
|
|
27
|
+
if global_dir.exists():
|
|
28
|
+
return global_dir
|
|
29
|
+
return global_json
|
|
30
|
+
|
|
31
|
+
base_dir = project_path if project_path is not None else Path.cwd()
|
|
32
|
+
ws_json = base_dir / ".claude.json"
|
|
33
|
+
if ws_json.exists():
|
|
34
|
+
return ws_json
|
|
35
|
+
ws_dir = base_dir / ".claude"
|
|
36
|
+
if ws_dir.exists():
|
|
37
|
+
return ws_dir
|
|
38
|
+
return ws_json
|
|
39
|
+
|
|
40
|
+
def get_skill_directory(self, scope: Scope, project_path: Path | None = None) -> Path:
|
|
41
|
+
"""Get the skill installation directory for Claude Code based on scope."""
|
|
42
|
+
if scope == Scope.GLOBAL:
|
|
43
|
+
return Path.home() / ".claude" / "skills"
|
|
44
|
+
base_dir = project_path if project_path is not None else Path.cwd()
|
|
45
|
+
return base_dir / ".claude" / "skills"
|
|
46
|
+
|
|
47
|
+
def detect(self, project_path: Path | None = None) -> AgentDetectionResult:
|
|
48
|
+
"""Detect Claude Code installation status, binary path, version, and config location."""
|
|
49
|
+
exec_path = find_executable("claude")
|
|
50
|
+
global_config = self.get_config_path(Scope.GLOBAL)
|
|
51
|
+
workspace_config = self.get_config_path(Scope.WORKSPACE, project_path=project_path)
|
|
52
|
+
|
|
53
|
+
# Check if config exists on disk
|
|
54
|
+
has_global = global_config is not None and global_config.exists()
|
|
55
|
+
has_workspace = workspace_config is not None and workspace_config.exists()
|
|
56
|
+
installed = (exec_path is not None) or has_global or has_workspace
|
|
57
|
+
|
|
58
|
+
version: str | None = None
|
|
59
|
+
detection_error: str | None = None
|
|
60
|
+
|
|
61
|
+
if exec_path is not None:
|
|
62
|
+
try:
|
|
63
|
+
version = run_version_command("claude")
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
detection_error = f"Error running version command: {exc}"
|
|
66
|
+
|
|
67
|
+
active_config = (
|
|
68
|
+
workspace_config
|
|
69
|
+
if (has_workspace and workspace_config)
|
|
70
|
+
else (global_config if (has_global and global_config) else None)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return AgentDetectionResult(
|
|
74
|
+
agent_id=self.agent_id,
|
|
75
|
+
name=self.name,
|
|
76
|
+
installed=installed,
|
|
77
|
+
version=version,
|
|
78
|
+
executable_path=str(exec_path) if exec_path else None,
|
|
79
|
+
global_config_path=str(global_config) if global_config else None,
|
|
80
|
+
workspace_config_path=str(workspace_config) if workspace_config else None,
|
|
81
|
+
config_path=str(active_config) if active_config else None,
|
|
82
|
+
supported_scopes=[Scope.GLOBAL, Scope.WORKSPACE],
|
|
83
|
+
capabilities=list(self.capabilities),
|
|
84
|
+
detection_error=detection_error,
|
|
85
|
+
)
|
aiaddons/agents/codex.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""OpenAI Codex agent detection adapter."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from aiaddons.agents.utils import find_executable, run_version_command
|
|
6
|
+
from aiaddons.core.models.agent import AgentCapability, AgentDetectionResult, Scope
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CodexAdapter:
|
|
10
|
+
"""Adapter for detecting and configuring OpenAI Codex CLI."""
|
|
11
|
+
|
|
12
|
+
agent_id: str = "codex"
|
|
13
|
+
name: str = "OpenAI Codex"
|
|
14
|
+
capabilities: list[AgentCapability] = [AgentCapability.MCP, AgentCapability.SKILL]
|
|
15
|
+
|
|
16
|
+
def supports_capability(self, capability: AgentCapability) -> bool:
|
|
17
|
+
"""Check if OpenAI Codex supports the given capability."""
|
|
18
|
+
return capability in self.capabilities
|
|
19
|
+
|
|
20
|
+
def get_config_path(self, scope: Scope, project_path: Path | None = None) -> Path | None:
|
|
21
|
+
"""Get the configuration path for OpenAI Codex based on scope."""
|
|
22
|
+
if scope == Scope.GLOBAL:
|
|
23
|
+
global_dir = Path.home() / ".codex"
|
|
24
|
+
if global_dir.exists():
|
|
25
|
+
return global_dir
|
|
26
|
+
return global_dir
|
|
27
|
+
|
|
28
|
+
base_dir = project_path if project_path is not None else Path.cwd()
|
|
29
|
+
ws_dir = base_dir / ".codex"
|
|
30
|
+
if ws_dir.exists():
|
|
31
|
+
return ws_dir
|
|
32
|
+
ws_agents = base_dir / ".agents"
|
|
33
|
+
if ws_agents.exists():
|
|
34
|
+
return ws_agents
|
|
35
|
+
return ws_dir
|
|
36
|
+
|
|
37
|
+
def get_skill_directory(self, scope: Scope, project_path: Path | None = None) -> Path:
|
|
38
|
+
"""Get the skill installation directory for OpenAI Codex based on scope."""
|
|
39
|
+
if scope == Scope.GLOBAL:
|
|
40
|
+
return Path.home() / ".codex" / "skills"
|
|
41
|
+
base_dir = project_path if project_path is not None else Path.cwd()
|
|
42
|
+
return base_dir / ".agents" / "skills"
|
|
43
|
+
|
|
44
|
+
def detect(self, project_path: Path | None = None) -> AgentDetectionResult:
|
|
45
|
+
"""Detect OpenAI Codex installation status, binary path, version, and config location."""
|
|
46
|
+
exec_path = find_executable("codex")
|
|
47
|
+
global_config = self.get_config_path(Scope.GLOBAL)
|
|
48
|
+
workspace_config = self.get_config_path(Scope.WORKSPACE, project_path=project_path)
|
|
49
|
+
|
|
50
|
+
has_global = global_config is not None and global_config.exists()
|
|
51
|
+
has_workspace = workspace_config is not None and workspace_config.exists()
|
|
52
|
+
installed = (exec_path is not None) or has_global or has_workspace
|
|
53
|
+
|
|
54
|
+
version: str | None = None
|
|
55
|
+
detection_error: str | None = None
|
|
56
|
+
|
|
57
|
+
if exec_path is not None:
|
|
58
|
+
try:
|
|
59
|
+
version = run_version_command("codex")
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
detection_error = f"Error running version command: {exc}"
|
|
62
|
+
|
|
63
|
+
active_config = (
|
|
64
|
+
workspace_config
|
|
65
|
+
if (has_workspace and workspace_config)
|
|
66
|
+
else (global_config if (has_global and global_config) else None)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return AgentDetectionResult(
|
|
70
|
+
agent_id=self.agent_id,
|
|
71
|
+
name=self.name,
|
|
72
|
+
installed=installed,
|
|
73
|
+
version=version,
|
|
74
|
+
executable_path=str(exec_path) if exec_path else None,
|
|
75
|
+
global_config_path=str(global_config) if global_config else None,
|
|
76
|
+
workspace_config_path=str(workspace_config) if workspace_config else None,
|
|
77
|
+
config_path=str(active_config) if active_config else None,
|
|
78
|
+
supported_scopes=[Scope.GLOBAL, Scope.WORKSPACE],
|
|
79
|
+
capabilities=list(self.capabilities),
|
|
80
|
+
detection_error=detection_error,
|
|
81
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Agent detection manager aggregating all registered AI agent adapters."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from aiaddons.agents.antigravity import AntigravityAdapter
|
|
7
|
+
from aiaddons.agents.base import BaseAgentAdapter
|
|
8
|
+
from aiaddons.agents.claude_code import ClaudeCodeAdapter
|
|
9
|
+
from aiaddons.agents.codex import CodexAdapter
|
|
10
|
+
from aiaddons.core.models.agent import AgentDetectionResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentDetectionManager:
|
|
14
|
+
"""Manager component for detecting installed AI coding agents."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, adapters: Sequence[BaseAgentAdapter] | None = None) -> None:
|
|
17
|
+
if adapters is None:
|
|
18
|
+
self._adapters: list[BaseAgentAdapter] = [
|
|
19
|
+
ClaudeCodeAdapter(),
|
|
20
|
+
CodexAdapter(),
|
|
21
|
+
AntigravityAdapter(),
|
|
22
|
+
]
|
|
23
|
+
else:
|
|
24
|
+
self._adapters = list(adapters)
|
|
25
|
+
|
|
26
|
+
def register_adapter(self, adapter: BaseAgentAdapter) -> None:
|
|
27
|
+
"""Register a new agent adapter."""
|
|
28
|
+
self._adapters.append(adapter)
|
|
29
|
+
|
|
30
|
+
def get_adapter(self, agent_id: str) -> BaseAgentAdapter | None:
|
|
31
|
+
"""Retrieve registered adapter for a given agent ID."""
|
|
32
|
+
aid = agent_id.lower()
|
|
33
|
+
for adapter in self._adapters:
|
|
34
|
+
if adapter.agent_id.lower() == aid:
|
|
35
|
+
return adapter
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def detect_agents(self, project_path: Path | None = None) -> dict[str, AgentDetectionResult]:
|
|
39
|
+
"""Detect all registered agents and return results mapped by agent ID."""
|
|
40
|
+
results: dict[str, AgentDetectionResult] = {}
|
|
41
|
+
for adapter in self._adapters:
|
|
42
|
+
try:
|
|
43
|
+
results[adapter.agent_id] = adapter.detect(project_path=project_path)
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
results[adapter.agent_id] = AgentDetectionResult(
|
|
46
|
+
agent_id=adapter.agent_id,
|
|
47
|
+
name=adapter.name,
|
|
48
|
+
installed=False,
|
|
49
|
+
capabilities=list(adapter.capabilities),
|
|
50
|
+
detection_error=f"Unexpected detection error: {exc}",
|
|
51
|
+
)
|
|
52
|
+
return results
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def detect_agents(project_path: Path | None = None) -> dict[str, AgentDetectionResult]:
|
|
56
|
+
"""Convenience function to run agent detection with default adapters."""
|
|
57
|
+
manager = AgentDetectionManager()
|
|
58
|
+
return manager.detect_agents(project_path=project_path)
|
aiaddons/agents/utils.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Helper utilities for agent detection and CLI binary version parsing."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def find_executable(name: str) -> Path | None:
|
|
10
|
+
"""Find executable in system PATH using shutil.which, returning absolute Path or None."""
|
|
11
|
+
found = shutil.which(name)
|
|
12
|
+
if found:
|
|
13
|
+
return Path(found)
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_version_output(output: str) -> str | None:
|
|
18
|
+
"""Extract a SemVer-like version string from raw command output."""
|
|
19
|
+
if not output:
|
|
20
|
+
return None
|
|
21
|
+
match = re.search(r"v?(\d+\.\d+\.\d+(?:-[\w.-]+)?)", output)
|
|
22
|
+
if match:
|
|
23
|
+
return match.group(1)
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_version_command(
|
|
28
|
+
cmd: str, args: list[str] | None = None, timeout_seconds: float = 5.0
|
|
29
|
+
) -> str | None:
|
|
30
|
+
"""Safely run executable version command without shell=True and return parsed version."""
|
|
31
|
+
executable_path = find_executable(cmd)
|
|
32
|
+
if executable_path is None:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
full_cmd = [str(executable_path)] + (args if args is not None else ["--version"])
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
res = subprocess.run(
|
|
39
|
+
full_cmd,
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
timeout=timeout_seconds,
|
|
43
|
+
check=False,
|
|
44
|
+
)
|
|
45
|
+
output_text = res.stdout if res.stdout else res.stderr
|
|
46
|
+
return parse_version_output(output_text)
|
|
47
|
+
except (subprocess.SubprocessError, OSError, ValueError):
|
|
48
|
+
return None
|
aiaddons/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands subpackage."""
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""CLI command for listing and detecting AI coding agents."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from aiaddons.agents.manager import detect_agents
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_symbols() -> tuple[str, str]:
|
|
16
|
+
"""Return platform and encoding safe status symbols."""
|
|
17
|
+
try:
|
|
18
|
+
"✓".encode(sys.stdout.encoding or "utf-8")
|
|
19
|
+
return "✓", "✗"
|
|
20
|
+
except (UnicodeEncodeError, AttributeError, TypeError):
|
|
21
|
+
return "+", "-"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def agents_command(
|
|
25
|
+
json_output: bool = typer.Option(
|
|
26
|
+
False,
|
|
27
|
+
"--json",
|
|
28
|
+
"-j",
|
|
29
|
+
help="Output raw detection results in JSON format.",
|
|
30
|
+
),
|
|
31
|
+
project_path: Path | None = typer.Option(
|
|
32
|
+
None,
|
|
33
|
+
"--project-path",
|
|
34
|
+
"-p",
|
|
35
|
+
help="Specify custom project directory for detection.",
|
|
36
|
+
),
|
|
37
|
+
) -> None:
|
|
38
|
+
"""List detected AI coding agents and their installation status."""
|
|
39
|
+
results = detect_agents(project_path=project_path)
|
|
40
|
+
|
|
41
|
+
if json_output:
|
|
42
|
+
serialized = {k: v.model_dump() for k, v in results.items()}
|
|
43
|
+
sys.stdout.write(json.dumps(serialized, indent=2) + "\n")
|
|
44
|
+
sys.stdout.flush()
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
console.print("\n[bold cyan]AI Coding Agents[/bold cyan]\n")
|
|
48
|
+
check_sym, cross_sym = _get_symbols()
|
|
49
|
+
|
|
50
|
+
for _agent_id, res in results.items():
|
|
51
|
+
if res.installed:
|
|
52
|
+
console.print(f"[bold green]{check_sym}[/bold green] [bold]{res.name}[/bold]")
|
|
53
|
+
if res.version:
|
|
54
|
+
console.print(f" [dim]Version:[/dim] {res.version}")
|
|
55
|
+
else:
|
|
56
|
+
console.print(" [dim]Version:[/dim] [yellow]Unknown[/yellow]")
|
|
57
|
+
|
|
58
|
+
if res.executable_path:
|
|
59
|
+
console.print(f" [dim]Path:[/dim] {res.executable_path}")
|
|
60
|
+
|
|
61
|
+
if res.config_path:
|
|
62
|
+
console.print(f" [dim]Config:[/dim] {res.config_path}")
|
|
63
|
+
|
|
64
|
+
if res.capabilities:
|
|
65
|
+
caps_str = ", ".join(c.value for c in res.capabilities)
|
|
66
|
+
console.print(f" [dim]Capabilities:[/dim] {caps_str}")
|
|
67
|
+
else:
|
|
68
|
+
console.print(f"[bold red]{cross_sym}[/bold red] [bold]{res.name}[/bold]")
|
|
69
|
+
console.print(" [dim]Not installed[/dim]")
|
|
70
|
+
|
|
71
|
+
if res.detection_error:
|
|
72
|
+
console.print(f" [red]Detection Note:[/red] {res.detection_error}")
|
|
73
|
+
|
|
74
|
+
console.print()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""CLI command for checking add-on compatibility against detected AI coding agents."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from aiaddons.agents.manager import AgentDetectionManager
|
|
11
|
+
from aiaddons.core.compatibility.engine import CompatibilityEngine
|
|
12
|
+
from aiaddons.core.models.agent import Scope
|
|
13
|
+
from aiaddons.registry.registry import Registry
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_symbols() -> tuple[str, str]:
|
|
19
|
+
"""Return platform-safe symbols for checkmark and cross mark."""
|
|
20
|
+
try:
|
|
21
|
+
"✓".encode(sys.stdout.encoding or "utf-8")
|
|
22
|
+
return "✓", "✗"
|
|
23
|
+
except Exception:
|
|
24
|
+
return "+", "-"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_command(
|
|
28
|
+
addon_id: str = typer.Argument(..., help="Add-on ID to evaluate compatibility for."),
|
|
29
|
+
scope: str = typer.Option(
|
|
30
|
+
"workspace",
|
|
31
|
+
"--scope",
|
|
32
|
+
"-s",
|
|
33
|
+
help="Requested installation scope (global or workspace).",
|
|
34
|
+
),
|
|
35
|
+
json_output: bool = typer.Option(
|
|
36
|
+
False,
|
|
37
|
+
"--json",
|
|
38
|
+
"-j",
|
|
39
|
+
help="Output evaluation results in JSON format.",
|
|
40
|
+
),
|
|
41
|
+
registry_dir: Path | None = typer.Option(
|
|
42
|
+
None,
|
|
43
|
+
"--registry-dir",
|
|
44
|
+
"-r",
|
|
45
|
+
help="Path to custom registry directory.",
|
|
46
|
+
),
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Evaluate compatibility of an add-on against all detected AI coding agents (read-only)."""
|
|
49
|
+
try:
|
|
50
|
+
req_scope = Scope.from_str(scope)
|
|
51
|
+
except ValueError as e:
|
|
52
|
+
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
53
|
+
raise typer.Exit(code=1) from e
|
|
54
|
+
|
|
55
|
+
registry, _ = Registry.load_auto(custom_dir=registry_dir)
|
|
56
|
+
|
|
57
|
+
manifest = registry.get(addon_id)
|
|
58
|
+
if not manifest:
|
|
59
|
+
console.print(f"[bold red]Error:[/bold red] Add-on '{addon_id}' not found in registry.")
|
|
60
|
+
raise typer.Exit(code=1)
|
|
61
|
+
|
|
62
|
+
manager = AgentDetectionManager()
|
|
63
|
+
detected_agents = list(manager.detect_agents().values())
|
|
64
|
+
|
|
65
|
+
engine = CompatibilityEngine(registry=registry)
|
|
66
|
+
results = engine.evaluate_all(
|
|
67
|
+
manifest, detected_agents, requested_scope=req_scope, registry=registry
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if json_output:
|
|
71
|
+
serialized = [r.model_dump() for r in results]
|
|
72
|
+
sys.stdout.write(json.dumps(serialized, indent=2) + "\n")
|
|
73
|
+
sys.stdout.flush()
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
sym_ok, sym_fail = _get_symbols()
|
|
77
|
+
|
|
78
|
+
console.print(
|
|
79
|
+
f"\n[bold cyan]Compatibility Evaluation for '{manifest.name}' ({manifest.id})[/bold cyan]"
|
|
80
|
+
)
|
|
81
|
+
console.print(f"[bold]Requested Scope:[/bold] {req_scope.value}\n")
|
|
82
|
+
|
|
83
|
+
for r in results:
|
|
84
|
+
status_str = (
|
|
85
|
+
f"[bold green]{sym_ok} Compatible[/bold green]"
|
|
86
|
+
if r.compatible
|
|
87
|
+
else f"[bold red]{sym_fail} Incompatible[/bold red]"
|
|
88
|
+
)
|
|
89
|
+
console.print(f"[bold]{r.agent_name}[/bold] ({r.agent_id}): {status_str}")
|
|
90
|
+
|
|
91
|
+
for reason in r.reasons:
|
|
92
|
+
color = "green" if r.compatible else "yellow"
|
|
93
|
+
console.print(f" [{color}]• {reason}[/{color}]")
|
|
94
|
+
|
|
95
|
+
if r.missing_requirements:
|
|
96
|
+
console.print(" [bold red]Missing Requirements:[/bold red]")
|
|
97
|
+
for req in r.missing_requirements:
|
|
98
|
+
console.print(f" - {req}")
|
|
99
|
+
|
|
100
|
+
if r.unsupported_requirements:
|
|
101
|
+
console.print(" [bold red]Unsupported Requirements:[/bold red]")
|
|
102
|
+
for unsupp in r.unsupported_requirements:
|
|
103
|
+
console.print(f" - {unsupp}")
|
|
104
|
+
|
|
105
|
+
if r.warnings:
|
|
106
|
+
console.print(" [bold yellow]Warnings:[/bold yellow]")
|
|
107
|
+
for warn in r.warnings:
|
|
108
|
+
console.print(f" - {warn}")
|
|
109
|
+
|
|
110
|
+
console.print()
|