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.
- agentnet_cli/__init__.py +6 -0
- agentnet_cli/agents/README.md +55 -0
- agentnet_cli/agents/__init__.py +0 -0
- agentnet_cli/agents/base.py +37 -0
- agentnet_cli/agents/claude.py +114 -0
- agentnet_cli/agents/codex.py +78 -0
- agentnet_cli/agents/copilot.py +89 -0
- agentnet_cli/agents/cursor.py +90 -0
- agentnet_cli/agents/hermes.py +171 -0
- agentnet_cli/agents/openclaw.py +54 -0
- agentnet_cli/agents/registry.py +28 -0
- agentnet_cli/agents/shims.py +9 -0
- agentnet_cli/agents/vscode.py +119 -0
- agentnet_cli/commands/__init__.py +0 -0
- agentnet_cli/commands/agent.py +32 -0
- agentnet_cli/commands/discover.py +34 -0
- agentnet_cli/commands/session.py +35 -0
- agentnet_cli/commands/wallet.py +48 -0
- agentnet_cli/config.py +68 -0
- agentnet_cli/connect.py +73 -0
- agentnet_cli/detect.py +23 -0
- agentnet_cli/disconnect.py +60 -0
- agentnet_cli/hermes_plugin/__init__.py +36 -0
- agentnet_cli/hermes_plugin/handlers.py +69 -0
- agentnet_cli/hermes_plugin/plugin.yaml +17 -0
- agentnet_cli/hermes_plugin/schemas.py +182 -0
- agentnet_cli/hermes_plugin/skills/agentnet/SKILL.md +46 -0
- agentnet_cli/main.py +263 -0
- agentnet_cli/manifest.py +58 -0
- agentnet_cli/marketplace.py +39 -0
- agentnet_cli/mcp/README.md +64 -0
- agentnet_cli/mcp/__init__.py +0 -0
- agentnet_cli/mcp/server.py +244 -0
- agentnet_cli/mcp/tools.py +67 -0
- agentnet_cli/paths.py +91 -0
- agentnet_cli/platform/README.md +79 -0
- agentnet_cli/platform/__init__.py +0 -0
- agentnet_cli/platform/client.py +134 -0
- agentnet_cli/register.py +146 -0
- agentnet_cli/shims/README.md +48 -0
- agentnet_cli/shims/SKILL.md +225 -0
- agentnet_cli/shims/codex/skill.md +8 -0
- agentnet_cli/shims/copilot/agentnet.agent.md +14 -0
- agentnet_cli/shims/cursor/agent.md +9 -0
- agentnet_cli/shims/cursor/agentnet.mdc +8 -0
- agentnet_cli/shims/shared/context.md +62 -0
- agentnet_cli/shims/vscode/instructions.md +3 -0
- agentnet_cli/status.py +65 -0
- agentnet_cli/updater.py +123 -0
- agentnet_cli-0.1.0.dist-info/METADATA +253 -0
- agentnet_cli-0.1.0.dist-info/RECORD +53 -0
- agentnet_cli-0.1.0.dist-info/WHEEL +4 -0
- agentnet_cli-0.1.0.dist-info/entry_points.txt +5 -0
agentnet_cli/__init__.py
ADDED
|
@@ -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")
|