agentnet-cli 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.
Files changed (53) hide show
  1. agentnet_cli/__init__.py +6 -0
  2. agentnet_cli/agents/README.md +55 -0
  3. agentnet_cli/agents/__init__.py +0 -0
  4. agentnet_cli/agents/base.py +37 -0
  5. agentnet_cli/agents/claude.py +114 -0
  6. agentnet_cli/agents/codex.py +78 -0
  7. agentnet_cli/agents/copilot.py +89 -0
  8. agentnet_cli/agents/cursor.py +90 -0
  9. agentnet_cli/agents/hermes.py +171 -0
  10. agentnet_cli/agents/openclaw.py +54 -0
  11. agentnet_cli/agents/registry.py +28 -0
  12. agentnet_cli/agents/shims.py +9 -0
  13. agentnet_cli/agents/vscode.py +119 -0
  14. agentnet_cli/commands/__init__.py +0 -0
  15. agentnet_cli/commands/agent.py +32 -0
  16. agentnet_cli/commands/discover.py +34 -0
  17. agentnet_cli/commands/session.py +35 -0
  18. agentnet_cli/commands/wallet.py +48 -0
  19. agentnet_cli/config.py +68 -0
  20. agentnet_cli/connect.py +73 -0
  21. agentnet_cli/detect.py +23 -0
  22. agentnet_cli/disconnect.py +60 -0
  23. agentnet_cli/hermes_plugin/__init__.py +36 -0
  24. agentnet_cli/hermes_plugin/handlers.py +69 -0
  25. agentnet_cli/hermes_plugin/plugin.yaml +17 -0
  26. agentnet_cli/hermes_plugin/schemas.py +182 -0
  27. agentnet_cli/hermes_plugin/skills/agentnet/SKILL.md +46 -0
  28. agentnet_cli/main.py +263 -0
  29. agentnet_cli/manifest.py +58 -0
  30. agentnet_cli/marketplace.py +39 -0
  31. agentnet_cli/mcp/README.md +64 -0
  32. agentnet_cli/mcp/__init__.py +0 -0
  33. agentnet_cli/mcp/server.py +244 -0
  34. agentnet_cli/mcp/tools.py +67 -0
  35. agentnet_cli/paths.py +91 -0
  36. agentnet_cli/platform/README.md +79 -0
  37. agentnet_cli/platform/__init__.py +0 -0
  38. agentnet_cli/platform/client.py +134 -0
  39. agentnet_cli/register.py +146 -0
  40. agentnet_cli/shims/README.md +48 -0
  41. agentnet_cli/shims/SKILL.md +225 -0
  42. agentnet_cli/shims/codex/skill.md +8 -0
  43. agentnet_cli/shims/copilot/agentnet.agent.md +14 -0
  44. agentnet_cli/shims/cursor/agent.md +9 -0
  45. agentnet_cli/shims/cursor/agentnet.mdc +8 -0
  46. agentnet_cli/shims/shared/context.md +62 -0
  47. agentnet_cli/shims/vscode/instructions.md +3 -0
  48. agentnet_cli/status.py +65 -0
  49. agentnet_cli/updater.py +123 -0
  50. agentnet_cli-0.1.0.dist-info/METADATA +253 -0
  51. agentnet_cli-0.1.0.dist-info/RECORD +53 -0
  52. agentnet_cli-0.1.0.dist-info/WHEEL +4 -0
  53. agentnet_cli-0.1.0.dist-info/entry_points.txt +5 -0
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("agentnet-cli")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.0.0-dev"
@@ -0,0 +1,55 @@
1
+ # Agent Connectors
2
+
3
+ Each file in this directory implements integration with one AI coding agent. All connectors implement the `AgentConnector` ABC from `base.py`.
4
+
5
+ ## Connector Interface
6
+
7
+ ```python
8
+ class AgentConnector(ABC):
9
+ def detect(self) -> DetectionResult
10
+ def connect(self, platform_config: dict) -> ConnectionResult
11
+ def disconnect(self, connection_manifest: dict) -> bool
12
+ ```
13
+
14
+ **detect()** — Check if the agent is installed by looking for its config directory and validation files.
15
+
16
+ **connect()** — Inject three layers:
17
+ 1. MCP server config (so the agent can call Agent-net tools)
18
+ 2. Context/skill files (so the LLM knows how to use the tools)
19
+ 3. Permission rules (so tool calls don't require manual approval)
20
+
21
+ **disconnect()** — Remove everything `connect()` wrote, using the manifest to know exactly what to clean up.
22
+
23
+ ## Adding a New Connector
24
+
25
+ 1. Create `agents/<name>.py` implementing `AgentConnector`
26
+ 2. Add the agent to `paths.py`:
27
+ - Add to `AgentName(StrEnum)`
28
+ - Add to `_AGENT_DOT_DIRS` mapping
29
+ 3. Create shim templates in `shims/<name>/`
30
+ 4. Wire into `registry.py` `_CONNECTORS` dict
31
+ 5. Add tests in `tests/test_<name>.py`
32
+
33
+ ## Current Connectors
34
+
35
+ | File | Agent | MCP Config Path | Key Files Injected |
36
+ |------|-------|----------------|-------------------|
37
+ | `claude.py` | Claude Code | `~/.claude.json` | `skills/agentnet/SKILL.md`, `settings.json` perms |
38
+ | `cursor.py` | Cursor | `~/.cursor/mcp.json` | `rules/agentnet.mdc`, `agents/agentnet.md` |
39
+ | `copilot.py` | GitHub Copilot | `~/.copilot/mcp-config.json` | `agents/agentnet.agent.md` |
40
+ | `codex.py` | OpenAI Codex | `~/.codex/config.toml` | `skills/agentnet/SKILL.md` |
41
+ | `hermes.py` | Hermes (Nous) | `~/.hermes/config.yaml` | YAML merge under `mcp.servers` |
42
+ | `openclaw.py` | OpenClaw | `~/.openclaw/openclaw.json` | Plugin entry in `plugins` |
43
+
44
+ ## Path Resolution
45
+
46
+ All connectors use `agent_config_root()` and `agentnet_home()` from `paths.py` — never `Path.home()` directly. This ensures tests can monkeypatch the home directory.
47
+
48
+ ## Config Merging
49
+
50
+ When writing MCP configs, connectors merge into existing files (never overwrite):
51
+ - **JSON**: Read, deep-merge the `mcpServers.agentnet` key, write back
52
+ - **TOML**: Append `[mcp_servers.agentnet]` section if not present
53
+ - **YAML**: Read, merge under `mcp.servers.agentnet`, write back
54
+
55
+ Original files are backed up to `~/.agentnet/backups/<agent>/` before modification.
File without changes
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class DetectionResult:
11
+ agent_name: str
12
+ detected: bool
13
+ config_root: Path | None = None
14
+ binary_path: Path | None = None
15
+ binary_found: bool = False
16
+ version: str | None = None
17
+ already_connected: bool = False
18
+
19
+
20
+ @dataclass
21
+ class ConnectionResult:
22
+ success: bool
23
+ files_created: list[Path] = field(default_factory=list)
24
+ files_modified: list[tuple[Path, Path]] = field(default_factory=list)
25
+ mcp_entry: dict[str, Any] = field(default_factory=dict)
26
+ errors: list[str] = field(default_factory=list)
27
+
28
+
29
+ class AgentConnector(ABC):
30
+ @abstractmethod
31
+ def detect(self) -> DetectionResult: ...
32
+
33
+ @abstractmethod
34
+ def connect(self, platform_config: dict[str, Any]) -> ConnectionResult: ...
35
+
36
+ @abstractmethod
37
+ def disconnect(self, connection_manifest: dict[str, Any]) -> bool: ...
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ..paths import AgentName, agent_config_root
10
+ from .base import AgentConnector, ConnectionResult, DetectionResult
11
+
12
+ _GITHUB_MARKETPLACE = "TheAgent-net/agentnet-cli"
13
+ _PLUGIN_ID = "agentnet@agentnet-cli"
14
+ _SUBPROCESS_TIMEOUT = 120
15
+
16
+
17
+ def _find_marketplace_source() -> str:
18
+ local = Path(__file__).resolve().parent.parent.parent.parent
19
+ if (local / ".claude-plugin" / "marketplace.json").exists():
20
+ return str(local)
21
+ return _GITHUB_MARKETPLACE
22
+
23
+
24
+ class ClaudeConnector(AgentConnector):
25
+ def detect(self) -> DetectionResult:
26
+ root = agent_config_root(AgentName.CLAUDE)
27
+ if not root.exists():
28
+ return DetectionResult(agent_name=AgentName.CLAUDE, detected=False)
29
+ for vf in ["settings.json"]:
30
+ if (root / vf).exists():
31
+ return DetectionResult(agent_name=AgentName.CLAUDE, detected=True, config_root=root)
32
+ claude_json = root.parent / ".claude.json"
33
+ if claude_json.exists():
34
+ return DetectionResult(agent_name=AgentName.CLAUDE, detected=True, config_root=root)
35
+ return DetectionResult(agent_name=AgentName.CLAUDE, detected=False)
36
+
37
+ def connect(self, platform_config: dict[str, Any]) -> ConnectionResult:
38
+ claude_bin = shutil.which("claude")
39
+ if not claude_bin:
40
+ return ConnectionResult(
41
+ success=False,
42
+ errors=["Claude Code not found. Install it from https://code.claude.com"],
43
+ )
44
+
45
+ marketplace_src = _find_marketplace_source()
46
+
47
+ proc = subprocess.run(
48
+ ["claude", "plugin", "marketplace", "add", marketplace_src, "--scope", "user"],
49
+ capture_output=True,
50
+ timeout=_SUBPROCESS_TIMEOUT,
51
+ )
52
+ if proc.returncode != 0:
53
+ msg = proc.stderr.decode(errors="replace").strip()
54
+ return ConnectionResult(success=False, errors=[f"marketplace add failed: {msg}"])
55
+
56
+ proc = subprocess.run(
57
+ ["claude", "plugin", "install", _PLUGIN_ID, "--scope", "user"],
58
+ capture_output=True,
59
+ timeout=_SUBPROCESS_TIMEOUT,
60
+ )
61
+ if proc.returncode != 0:
62
+ msg = proc.stderr.decode(errors="replace").strip()
63
+ return ConnectionResult(success=False, errors=[f"plugin install failed: {msg}"])
64
+
65
+ self._cleanup_legacy()
66
+
67
+ return ConnectionResult(
68
+ success=True,
69
+ mcp_entry={"scope": "plugin", "plugin_name": _PLUGIN_ID},
70
+ )
71
+
72
+ def disconnect(self, connection_manifest: dict[str, Any]) -> bool:
73
+ claude_bin = shutil.which("claude")
74
+ if not claude_bin:
75
+ return True
76
+
77
+ subprocess.run(
78
+ ["claude", "plugin", "uninstall", _PLUGIN_ID, "--scope", "user", "-y"],
79
+ capture_output=True,
80
+ timeout=_SUBPROCESS_TIMEOUT,
81
+ )
82
+ return True
83
+
84
+ @staticmethod
85
+ def _cleanup_legacy() -> None:
86
+ root = agent_config_root(AgentName.CLAUDE)
87
+
88
+ skill_path = root / "skills" / "agentnet" / "SKILL.md"
89
+ if skill_path.exists():
90
+ skill_path.unlink()
91
+ skill_dir = skill_path.parent
92
+ if skill_dir.exists() and not any(skill_dir.iterdir()):
93
+ skill_dir.rmdir()
94
+
95
+ claude_json = root.parent / ".claude.json"
96
+ if claude_json.exists():
97
+ try:
98
+ data = json.loads(claude_json.read_text())
99
+ if "agentnet" in data.get("mcpServers", {}):
100
+ data["mcpServers"].pop("agentnet")
101
+ claude_json.write_text(json.dumps(data, indent=2) + "\n")
102
+ except (json.JSONDecodeError, OSError):
103
+ pass
104
+
105
+ settings_path = root / "settings.json"
106
+ if settings_path.exists():
107
+ try:
108
+ data = json.loads(settings_path.read_text())
109
+ allow = data.get("permissions", {}).get("allow", [])
110
+ if "mcp__agentnet__*" in allow:
111
+ allow.remove("mcp__agentnet__*")
112
+ settings_path.write_text(json.dumps(data, indent=2) + "\n")
113
+ except (json.JSONDecodeError, OSError):
114
+ pass
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import tomllib
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import tomli_w
9
+
10
+ from ..paths import AgentName, agent_config_root
11
+ from .base import AgentConnector, ConnectionResult, DetectionResult
12
+ from .shims import load_shim
13
+
14
+
15
+ class CodexConnector(AgentConnector):
16
+ def detect(self) -> DetectionResult:
17
+ root = agent_config_root(AgentName.CODEX)
18
+ if not root.exists():
19
+ return DetectionResult(agent_name=AgentName.CODEX, detected=False)
20
+ for vf in ["config.toml", "auth.json"]:
21
+ if (root / vf).exists():
22
+ return DetectionResult(agent_name=AgentName.CODEX, detected=True, config_root=root)
23
+ return DetectionResult(agent_name=AgentName.CODEX, detected=False)
24
+
25
+ def connect(self, platform_config: dict[str, Any]) -> ConnectionResult:
26
+ files_created: list[Path] = []
27
+ root = agent_config_root(AgentName.CODEX)
28
+ root.mkdir(parents=True, exist_ok=True)
29
+
30
+ toml_path = root / "config.toml"
31
+ data: dict[str, Any] = {}
32
+ if toml_path.exists():
33
+ data = tomllib.loads(toml_path.read_text())
34
+
35
+ agentnet_bin = shutil.which("agentnet")
36
+ if agentnet_bin:
37
+ command = agentnet_bin
38
+ else:
39
+ command = "uvx"
40
+
41
+ mcp_servers = data.setdefault("mcp_servers", {})
42
+ if command == "uvx":
43
+ mcp_servers["agentnet"] = {
44
+ "command": command,
45
+ "args": ["agentnet-cli", "mcp-serve"],
46
+ "env": {"AGENTNET_TOKEN": "${AGENTNET_TOKEN}"},
47
+ }
48
+ else:
49
+ mcp_servers["agentnet"] = {
50
+ "command": command,
51
+ "args": ["mcp-serve"],
52
+ }
53
+ toml_path.write_text(tomli_w.dumps(data))
54
+
55
+ skill_dir = root / "skills" / "agentnet"
56
+ skill_dir.mkdir(parents=True, exist_ok=True)
57
+ skill_path = skill_dir / "SKILL.md"
58
+ skill_path.write_text(load_shim("codex/skill.md"))
59
+ files_created.append(skill_path)
60
+ return ConnectionResult(
61
+ success=True, files_created=files_created,
62
+ mcp_entry={"scope": "user", "file": str(toml_path), "server_name": "agentnet"},
63
+ )
64
+
65
+ def disconnect(self, connection_manifest: dict[str, Any]) -> bool:
66
+ for path_str in connection_manifest.get("files_created", []):
67
+ p = Path(path_str)
68
+ if p.exists():
69
+ p.unlink()
70
+ mcp_info = connection_manifest.get("mcp_registered", {})
71
+ mcp_file = mcp_info.get("file")
72
+ if mcp_file:
73
+ toml_path = Path(mcp_file)
74
+ if toml_path.exists():
75
+ data = tomllib.loads(toml_path.read_text())
76
+ data.get("mcp_servers", {}).pop("agentnet", None)
77
+ toml_path.write_text(tomli_w.dumps(data))
78
+ return True
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from ..paths import AgentName, agent_config_root
9
+ from .base import AgentConnector, ConnectionResult, DetectionResult
10
+ from .shims import load_shim
11
+
12
+
13
+ class CopilotConnector(AgentConnector):
14
+ def detect(self) -> DetectionResult:
15
+ root = agent_config_root(AgentName.COPILOT)
16
+ if not root.exists():
17
+ return DetectionResult(agent_name=AgentName.COPILOT, detected=False)
18
+ if any((root / vf).exists() for vf in ["settings.json", "config.json", "mcp-config.json", "ide"]):
19
+ return DetectionResult(agent_name=AgentName.COPILOT, detected=True, config_root=root)
20
+ return DetectionResult(agent_name=AgentName.COPILOT, detected=False)
21
+
22
+ def connect(self, platform_config: dict[str, Any]) -> ConnectionResult:
23
+ files_created: list[Path] = []
24
+
25
+ root = agent_config_root(AgentName.COPILOT)
26
+ root.mkdir(parents=True, exist_ok=True)
27
+
28
+ dot_mcp = root / "mcp-config.json"
29
+ mcp_entry_info = self._merge_mcp(dot_mcp, self._build_mcp_entry())
30
+
31
+ agents_dir = root / "agents"
32
+ agents_dir.mkdir(parents=True, exist_ok=True)
33
+ agent_path = agents_dir / "agentnet.agent.md"
34
+ agent_path.write_text(load_shim("copilot/agentnet.agent.md"))
35
+ files_created.append(agent_path)
36
+
37
+ return ConnectionResult(
38
+ success=True,
39
+ files_created=files_created,
40
+ mcp_entry=mcp_entry_info,
41
+ )
42
+
43
+ def disconnect(self, connection_manifest: dict[str, Any]) -> bool:
44
+ deleted: list[Path] = []
45
+ for path_str in connection_manifest.get("files_created", []):
46
+ p = Path(path_str)
47
+ if p.exists():
48
+ p.unlink()
49
+ deleted.append(p)
50
+
51
+ # Clean empty parent dirs (e.g. agents/)
52
+ for p in deleted:
53
+ if p.parent.exists() and not any(p.parent.iterdir()):
54
+ p.parent.rmdir()
55
+
56
+ mcp_info = connection_manifest.get("mcp_registered", {})
57
+ mcp_file = mcp_info.get("file")
58
+ if mcp_file:
59
+ mcp_path = Path(mcp_file)
60
+ if mcp_path.exists():
61
+ data = json.loads(mcp_path.read_text())
62
+ data.get("mcpServers", {}).pop("agentnet", None)
63
+ mcp_path.write_text(json.dumps(data, indent=2) + "\n")
64
+
65
+ return True
66
+
67
+ def _build_mcp_entry(self) -> dict[str, Any]:
68
+ agentnet_bin = shutil.which("agentnet")
69
+ if agentnet_bin:
70
+ return {
71
+ "type": "stdio",
72
+ "command": agentnet_bin,
73
+ "args": ["mcp-serve"],
74
+ }
75
+ return {
76
+ "type": "stdio",
77
+ "command": "uvx",
78
+ "args": ["agentnet-cli", "mcp-serve"],
79
+ }
80
+
81
+ def _merge_mcp(self, mcp_path: Path, entry: dict[str, Any]) -> dict[str, Any]:
82
+ data: dict[str, Any] = {}
83
+ if mcp_path.exists():
84
+ data = json.loads(mcp_path.read_text())
85
+ data.setdefault("mcpServers", {})
86
+ data["mcpServers"]["agentnet"] = entry
87
+ mcp_path.parent.mkdir(parents=True, exist_ok=True)
88
+ mcp_path.write_text(json.dumps(data, indent=2) + "\n")
89
+ return {"scope": "user", "file": str(mcp_path), "server_name": "agentnet"}
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from ..paths import AgentName, agent_config_root
9
+ from .base import AgentConnector, ConnectionResult, DetectionResult
10
+ from .shims import load_shim
11
+
12
+
13
+ class CursorConnector(AgentConnector):
14
+ def detect(self) -> DetectionResult:
15
+ root = agent_config_root(AgentName.CURSOR)
16
+ if not root.exists():
17
+ return DetectionResult(agent_name=AgentName.CURSOR, detected=False)
18
+ for vf in ["extensions", "mcp.json", "cli-config.json"]:
19
+ if (root / vf).exists():
20
+ return DetectionResult(agent_name=AgentName.CURSOR, detected=True, config_root=root)
21
+ return DetectionResult(agent_name=AgentName.CURSOR, detected=False)
22
+
23
+ def connect(self, platform_config: dict[str, Any]) -> ConnectionResult:
24
+ files_created: list[Path] = []
25
+ root = agent_config_root(AgentName.CURSOR)
26
+
27
+ # Layer 1: MCP
28
+ mcp_path = root / "mcp.json"
29
+ mcp_entry = self._write_mcp(mcp_path)
30
+
31
+ # Layer 2a: Rule
32
+ rules_dir = root / "rules"
33
+ rules_dir.mkdir(parents=True, exist_ok=True)
34
+ mdc_path = rules_dir / "agentnet.mdc"
35
+ mdc_path.write_text(load_shim("cursor/agentnet.mdc"))
36
+ files_created.append(mdc_path)
37
+
38
+ # Layer 2b: Subagent
39
+ agents_dir = root / "agents"
40
+ agents_dir.mkdir(parents=True, exist_ok=True)
41
+ agent_path = agents_dir / "agentnet.md"
42
+ agent_path.write_text(load_shim("cursor/agent.md"))
43
+ files_created.append(agent_path)
44
+
45
+ return ConnectionResult(
46
+ success=True, files_created=files_created, mcp_entry=mcp_entry,
47
+ )
48
+
49
+ def disconnect(self, connection_manifest: dict[str, Any]) -> bool:
50
+ deleted: list[Path] = []
51
+ for path_str in connection_manifest.get("files_created", []):
52
+ p = Path(path_str)
53
+ if p.exists():
54
+ p.unlink()
55
+ deleted.append(p)
56
+
57
+ # Clean empty parent dirs (e.g. rules/, agents/)
58
+ for p in deleted:
59
+ if p.parent.exists() and not any(p.parent.iterdir()):
60
+ p.parent.rmdir()
61
+
62
+ mcp_info = connection_manifest.get("mcp_registered", {})
63
+ mcp_file = mcp_info.get("file")
64
+ if mcp_file:
65
+ mcp_path = Path(mcp_file)
66
+ if mcp_path.exists():
67
+ data = json.loads(mcp_path.read_text())
68
+ data.get("mcpServers", {}).pop("agentnet", None)
69
+ mcp_path.write_text(json.dumps(data, indent=2) + "\n")
70
+ return True
71
+
72
+ def _write_mcp(self, mcp_path: Path) -> dict[str, Any]:
73
+ data: dict[str, Any] = {}
74
+ if mcp_path.exists():
75
+ data = json.loads(mcp_path.read_text())
76
+ data.setdefault("mcpServers", {})
77
+
78
+ agentnet_bin = shutil.which("agentnet")
79
+ if agentnet_bin:
80
+ command, args = agentnet_bin, ["mcp-serve"]
81
+ else:
82
+ command, args = "uvx", ["agentnet-cli", "mcp-serve"]
83
+
84
+ data["mcpServers"]["agentnet"] = {
85
+ "command": command,
86
+ "args": args,
87
+ "env": {"AGENTNET_TOKEN": "${env:AGENTNET_TOKEN}"},
88
+ }
89
+ mcp_path.write_text(json.dumps(data, indent=2) + "\n")
90
+ return {"scope": "global", "file": str(mcp_path), "server_name": "agentnet"}
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+ from ..paths import AgentName, agent_config_root
11
+ from .base import AgentConnector, ConnectionResult, DetectionResult
12
+
13
+ _PLUGIN_NAME = "agentnet"
14
+
15
+
16
+ def _hermes_plugin_source() -> Path:
17
+ from ..hermes_plugin import _PLUGIN_DIR # noqa: PLC0415
18
+
19
+ return _PLUGIN_DIR
20
+
21
+
22
+ def _find_hermes_venv(root: Path) -> Path | None:
23
+ candidates = [
24
+ root / "hermes-agent" / "venv",
25
+ root / "venv",
26
+ ]
27
+ for venv in candidates:
28
+ if (venv / "bin" / "python").exists() or (venv / "Scripts" / "python.exe").exists():
29
+ return venv
30
+ return None
31
+
32
+
33
+ def _install_into_hermes_venv(venv: Path) -> bool:
34
+ python = venv / "bin" / "python"
35
+ if not python.exists():
36
+ python = venv / "Scripts" / "python.exe"
37
+ if not python.exists():
38
+ return False
39
+
40
+ try:
41
+ if shutil.which("uv"):
42
+ subprocess.run(
43
+ ["uv", "pip", "install", "agentnet-cli", "--python", str(python)],
44
+ capture_output=True,
45
+ timeout=120,
46
+ )
47
+ else:
48
+ subprocess.run(
49
+ [str(python), "-m", "pip", "install", "agentnet-cli"],
50
+ capture_output=True,
51
+ timeout=120,
52
+ )
53
+ return True
54
+ except Exception:
55
+ return False
56
+
57
+
58
+ class HermesConnector(AgentConnector):
59
+ def detect(self) -> DetectionResult:
60
+ root = agent_config_root(AgentName.HERMES)
61
+ if not root.exists():
62
+ return DetectionResult(agent_name=AgentName.HERMES, detected=False)
63
+ if (root / "config.yaml").exists():
64
+ return DetectionResult(
65
+ agent_name=AgentName.HERMES,
66
+ detected=True,
67
+ config_root=root,
68
+ )
69
+ return DetectionResult(agent_name=AgentName.HERMES, detected=False)
70
+
71
+ def connect(self, platform_config: dict[str, Any]) -> ConnectionResult:
72
+ root = agent_config_root(AgentName.HERMES)
73
+ config_path = root / "config.yaml"
74
+ plugin_dir = root / "plugins" / _PLUGIN_NAME
75
+
76
+ hermes_venv = _find_hermes_venv(root)
77
+ if hermes_venv:
78
+ _install_into_hermes_venv(hermes_venv)
79
+
80
+ source = _hermes_plugin_source()
81
+ if plugin_dir.exists():
82
+ shutil.rmtree(plugin_dir)
83
+ shutil.copytree(source, plugin_dir)
84
+
85
+ for cache_dir in plugin_dir.rglob("__pycache__"):
86
+ shutil.rmtree(cache_dir)
87
+
88
+ skill_src = source / "skills" / _PLUGIN_NAME
89
+ skill_dst = root / "skills" / _PLUGIN_NAME
90
+ if skill_src.is_dir():
91
+ if skill_dst.exists():
92
+ shutil.rmtree(skill_dst)
93
+ shutil.copytree(skill_src, skill_dst)
94
+
95
+ files_created = [f for f in plugin_dir.rglob("*") if f.is_file()]
96
+ if skill_dst.exists():
97
+ files_created.extend(f for f in skill_dst.rglob("*") if f.is_file())
98
+
99
+ data: dict[str, Any] = {}
100
+ if config_path.exists():
101
+ data = yaml.safe_load(config_path.read_text()) or {}
102
+
103
+ plugins = data.setdefault("plugins", {})
104
+ enabled = plugins.setdefault("enabled", [])
105
+ if _PLUGIN_NAME not in enabled:
106
+ enabled.append(_PLUGIN_NAME)
107
+
108
+ self._cleanup_legacy(data, root)
109
+
110
+ config_path.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False))
111
+
112
+ return ConnectionResult(
113
+ success=True,
114
+ files_created=files_created,
115
+ mcp_entry={
116
+ "scope": "plugin",
117
+ "plugin_dir": str(plugin_dir),
118
+ },
119
+ )
120
+
121
+ def disconnect(self, connection_manifest: dict[str, Any]) -> bool:
122
+ root = agent_config_root(AgentName.HERMES)
123
+ config_path = root / "config.yaml"
124
+
125
+ mcp_info = connection_manifest.get("mcp_registered", {})
126
+ plugin_dir_str = mcp_info.get("plugin_dir")
127
+ if plugin_dir_str:
128
+ plugin_dir = Path(plugin_dir_str)
129
+ else:
130
+ plugin_dir = root / "plugins" / _PLUGIN_NAME
131
+
132
+ if plugin_dir.exists():
133
+ shutil.rmtree(plugin_dir)
134
+
135
+ skill_dir = root / "skills" / _PLUGIN_NAME
136
+ if skill_dir.exists():
137
+ shutil.rmtree(skill_dir)
138
+
139
+ if config_path.exists():
140
+ data = yaml.safe_load(config_path.read_text()) or {}
141
+ plugins = data.get("plugins", {})
142
+ if isinstance(plugins, dict):
143
+ enabled = plugins.get("enabled", [])
144
+ if isinstance(enabled, list) and _PLUGIN_NAME in enabled:
145
+ enabled.remove(_PLUGIN_NAME)
146
+ self._cleanup_legacy(data, root)
147
+ config_path.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False))
148
+
149
+ return True
150
+
151
+ @staticmethod
152
+ def _cleanup_legacy(data: dict[str, Any], root: Path) -> None:
153
+ mcp_servers = data.get("mcp_servers", {})
154
+ if isinstance(mcp_servers, dict):
155
+ mcp_servers.pop("agentnet", None)
156
+
157
+ old_mcp = data.get("mcp", {})
158
+ if isinstance(old_mcp, dict):
159
+ old_servers = old_mcp.get("servers", {})
160
+ if isinstance(old_servers, dict):
161
+ old_servers.pop("agentnet", None)
162
+
163
+ platform_toolsets = data.get("platform_toolsets", {})
164
+ if isinstance(platform_toolsets, dict):
165
+ for toolsets in platform_toolsets.values():
166
+ if isinstance(toolsets, list) and "mcp-agentnet" in toolsets:
167
+ toolsets.remove("mcp-agentnet")
168
+
169
+ top_toolsets = data.get("toolsets")
170
+ if isinstance(top_toolsets, list) and "mcp-agentnet" in top_toolsets:
171
+ top_toolsets.remove("mcp-agentnet")