universal-agent-context 0.2.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.
- uacs/__init__.py +12 -0
- uacs/adapters/__init__.py +19 -0
- uacs/adapters/agent_skill_adapter.py +202 -0
- uacs/adapters/agents_md_adapter.py +330 -0
- uacs/adapters/base.py +261 -0
- uacs/adapters/clinerules_adapter.py +39 -0
- uacs/adapters/cursorrules_adapter.py +39 -0
- uacs/api.py +262 -0
- uacs/cli/__init__.py +6 -0
- uacs/cli/context.py +349 -0
- uacs/cli/main.py +195 -0
- uacs/cli/mcp.py +115 -0
- uacs/cli/memory.py +142 -0
- uacs/cli/packages.py +309 -0
- uacs/cli/skills.py +144 -0
- uacs/cli/utils.py +24 -0
- uacs/config/repositories.yaml +26 -0
- uacs/context/__init__.py +0 -0
- uacs/context/agent_context.py +406 -0
- uacs/context/shared_context.py +661 -0
- uacs/context/unified_context.py +332 -0
- uacs/mcp_server_entry.py +80 -0
- uacs/memory/__init__.py +5 -0
- uacs/memory/simple_memory.py +255 -0
- uacs/packages/__init__.py +26 -0
- uacs/packages/manager.py +413 -0
- uacs/packages/models.py +60 -0
- uacs/packages/sources.py +270 -0
- uacs/protocols/__init__.py +5 -0
- uacs/protocols/mcp/__init__.py +8 -0
- uacs/protocols/mcp/manager.py +77 -0
- uacs/protocols/mcp/skills_server.py +700 -0
- uacs/skills_validator.py +367 -0
- uacs/utils/__init__.py +5 -0
- uacs/utils/paths.py +24 -0
- uacs/visualization/README.md +132 -0
- uacs/visualization/__init__.py +36 -0
- uacs/visualization/models.py +195 -0
- uacs/visualization/static/index.html +857 -0
- uacs/visualization/storage.py +402 -0
- uacs/visualization/visualization.py +328 -0
- uacs/visualization/web_server.py +364 -0
- universal_agent_context-0.2.0.dist-info/METADATA +873 -0
- universal_agent_context-0.2.0.dist-info/RECORD +47 -0
- universal_agent_context-0.2.0.dist-info/WHEEL +4 -0
- universal_agent_context-0.2.0.dist-info/entry_points.txt +2 -0
- universal_agent_context-0.2.0.dist-info/licenses/LICENSE +21 -0
uacs/packages/sources.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Package source handlers for fetching packages from different sources.
|
|
2
|
+
|
|
3
|
+
Handles fetching packages from GitHub shorthand, Git URLs, and local paths.
|
|
4
|
+
Inspired by GitHub CLI extensions pattern.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
|
|
14
|
+
from uacs.packages.models import PackageSource
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PackageSourceError(Exception):
|
|
18
|
+
"""Base exception for package source operations."""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GitCloneError(PackageSourceError):
|
|
24
|
+
"""Raised when git clone operation fails."""
|
|
25
|
+
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LocalCopyError(PackageSourceError):
|
|
30
|
+
"""Raised when local copy operation fails."""
|
|
31
|
+
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InvalidSourceError(PackageSourceError):
|
|
36
|
+
"""Raised when source format is invalid."""
|
|
37
|
+
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PackageSourceHandler:
|
|
42
|
+
"""Handles fetching packages from different source types."""
|
|
43
|
+
|
|
44
|
+
# GitHub shorthand pattern: owner/repo
|
|
45
|
+
GITHUB_SHORTHAND_PATTERN = re.compile(r"^[a-zA-Z0-9][\w-]*/[\w.-]+$")
|
|
46
|
+
|
|
47
|
+
# Git URL patterns
|
|
48
|
+
GIT_URL_PATTERNS = [
|
|
49
|
+
re.compile(r"^https?://.*\.git$"), # HTTPS URLs ending in .git
|
|
50
|
+
re.compile(r"^git@.*:.*\.git$"), # SSH URLs
|
|
51
|
+
re.compile(r"^https?://github\.com/[\w-]+/[\w.-]+/?$"), # GitHub HTTPS
|
|
52
|
+
re.compile(r"^https?://gitlab\.com/[\w-]+/[\w.-]+/?$"), # GitLab HTTPS
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def detect_source_type(source: str) -> PackageSource:
|
|
57
|
+
"""Detect the type of package source.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
source: Source string (GitHub shorthand, Git URL, or local path)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
PackageSource enum value
|
|
64
|
+
"""
|
|
65
|
+
# Check for local paths first
|
|
66
|
+
if (
|
|
67
|
+
source.startswith("./")
|
|
68
|
+
or source.startswith("../")
|
|
69
|
+
or source.startswith("/")
|
|
70
|
+
):
|
|
71
|
+
return PackageSource.LOCAL
|
|
72
|
+
|
|
73
|
+
# Check for absolute Windows paths
|
|
74
|
+
if len(source) >= 3 and source[1:3] == ":\\":
|
|
75
|
+
return PackageSource.LOCAL
|
|
76
|
+
|
|
77
|
+
# Check for GitHub shorthand (owner/repo)
|
|
78
|
+
if PackageSourceHandler.GITHUB_SHORTHAND_PATTERN.match(source):
|
|
79
|
+
return PackageSource.GITHUB
|
|
80
|
+
|
|
81
|
+
# Check for Git URLs
|
|
82
|
+
for pattern in PackageSourceHandler.GIT_URL_PATTERNS:
|
|
83
|
+
if pattern.match(source):
|
|
84
|
+
return PackageSource.GIT_URL
|
|
85
|
+
|
|
86
|
+
# Try parsing as URL
|
|
87
|
+
try:
|
|
88
|
+
parsed = urlparse(source)
|
|
89
|
+
if parsed.scheme in ("http", "https", "git", "ssh"):
|
|
90
|
+
return PackageSource.GIT_URL
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
return PackageSource.UNKNOWN
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def parse_github_shorthand(shorthand: str) -> tuple[str, str]:
|
|
98
|
+
"""Parse GitHub shorthand into owner and repo.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
shorthand: GitHub shorthand string (e.g., "owner/repo")
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Tuple of (owner, repo)
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
InvalidSourceError: If shorthand format is invalid
|
|
108
|
+
"""
|
|
109
|
+
if not PackageSourceHandler.GITHUB_SHORTHAND_PATTERN.match(shorthand):
|
|
110
|
+
raise InvalidSourceError(
|
|
111
|
+
f"Invalid GitHub shorthand format: {shorthand}. "
|
|
112
|
+
"Expected format: owner/repo"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
parts = shorthand.split("/")
|
|
116
|
+
if len(parts) != 2:
|
|
117
|
+
raise InvalidSourceError(f"Invalid GitHub shorthand format: {shorthand}")
|
|
118
|
+
|
|
119
|
+
owner, repo = parts
|
|
120
|
+
return owner, repo
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def fetch_from_github(owner: str, repo: str, target_dir: Path) -> Path:
|
|
124
|
+
"""Clone GitHub repository to target directory.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
owner: GitHub repository owner
|
|
128
|
+
repo: GitHub repository name
|
|
129
|
+
target_dir: Directory to clone into
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Path to cloned repository
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
GitCloneError: If git clone operation fails
|
|
136
|
+
"""
|
|
137
|
+
url = f"https://github.com/{owner}/{repo}.git"
|
|
138
|
+
return PackageSourceHandler.fetch_from_git_url(url, target_dir)
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def fetch_from_git_url(url: str, target_dir: Path) -> Path:
|
|
142
|
+
"""Clone git repository from URL to target directory.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
url: Git repository URL
|
|
146
|
+
target_dir: Directory to clone into
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Path to cloned repository
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
GitCloneError: If git clone operation fails
|
|
153
|
+
"""
|
|
154
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# Run git clone command
|
|
158
|
+
subprocess.run(
|
|
159
|
+
["git", "clone", "--depth", "1", url, str(target_dir)],
|
|
160
|
+
capture_output=True,
|
|
161
|
+
text=True,
|
|
162
|
+
check=True,
|
|
163
|
+
)
|
|
164
|
+
except subprocess.CalledProcessError as e:
|
|
165
|
+
error_msg = e.stderr.strip() if e.stderr else str(e)
|
|
166
|
+
raise GitCloneError(
|
|
167
|
+
f"Failed to clone repository from {url}: {error_msg}"
|
|
168
|
+
) from e
|
|
169
|
+
except FileNotFoundError:
|
|
170
|
+
raise GitCloneError(
|
|
171
|
+
"git command not found. Please ensure git is installed and in PATH."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Verify the directory exists and has content
|
|
175
|
+
if not target_dir.exists() or not any(target_dir.iterdir()):
|
|
176
|
+
raise GitCloneError(f"Clone succeeded but directory is empty: {target_dir}")
|
|
177
|
+
|
|
178
|
+
return target_dir
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def fetch_from_local(source_path: Path, target_dir: Path) -> Path:
|
|
182
|
+
"""Copy local directory to target directory.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
source_path: Source directory path
|
|
186
|
+
target_dir: Target directory to copy to
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Path to copied directory
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
LocalCopyError: If copy operation fails
|
|
193
|
+
"""
|
|
194
|
+
# Resolve source path
|
|
195
|
+
try:
|
|
196
|
+
source_path = source_path.resolve()
|
|
197
|
+
except Exception as e:
|
|
198
|
+
raise LocalCopyError(f"Failed to resolve source path: {e}") from e
|
|
199
|
+
|
|
200
|
+
# Verify source exists
|
|
201
|
+
if not source_path.exists():
|
|
202
|
+
raise LocalCopyError(f"Source path does not exist: {source_path}")
|
|
203
|
+
|
|
204
|
+
if not source_path.is_dir():
|
|
205
|
+
raise LocalCopyError(f"Source path is not a directory: {source_path}")
|
|
206
|
+
|
|
207
|
+
# Create target directory
|
|
208
|
+
try:
|
|
209
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
raise LocalCopyError(f"Failed to create target directory: {e}") from e
|
|
212
|
+
|
|
213
|
+
# Copy directory contents
|
|
214
|
+
try:
|
|
215
|
+
shutil.copytree(source_path, target_dir, dirs_exist_ok=True)
|
|
216
|
+
except Exception as e:
|
|
217
|
+
raise LocalCopyError(
|
|
218
|
+
f"Failed to copy from {source_path} to {target_dir}: {e}"
|
|
219
|
+
) from e
|
|
220
|
+
|
|
221
|
+
return target_dir
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def fetch(
|
|
225
|
+
source: str, target_dir: Path | None = None
|
|
226
|
+
) -> tuple[Path, PackageSource]:
|
|
227
|
+
"""Fetch package from source to target directory.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
source: Package source (GitHub shorthand, Git URL, or local path)
|
|
231
|
+
target_dir: Target directory (creates temp dir if None)
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Tuple of (package_path, source_type)
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
InvalidSourceError: If source type cannot be determined
|
|
238
|
+
GitCloneError: If git clone operation fails
|
|
239
|
+
LocalCopyError: If local copy operation fails
|
|
240
|
+
"""
|
|
241
|
+
source_type = PackageSourceHandler.detect_source_type(source)
|
|
242
|
+
|
|
243
|
+
if source_type == PackageSource.UNKNOWN:
|
|
244
|
+
raise InvalidSourceError(
|
|
245
|
+
f"Unable to determine source type for: {source}. "
|
|
246
|
+
"Expected GitHub shorthand (owner/repo), Git URL, or local path."
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Create temp directory if not provided
|
|
250
|
+
if target_dir is None:
|
|
251
|
+
temp_dir = tempfile.mkdtemp(prefix="uacs-package-")
|
|
252
|
+
target_dir = Path(temp_dir)
|
|
253
|
+
|
|
254
|
+
# Fetch based on source type
|
|
255
|
+
if source_type == PackageSource.GITHUB:
|
|
256
|
+
owner, repo = PackageSourceHandler.parse_github_shorthand(source)
|
|
257
|
+
package_path = PackageSourceHandler.fetch_from_github(
|
|
258
|
+
owner, repo, target_dir
|
|
259
|
+
)
|
|
260
|
+
elif source_type == PackageSource.GIT_URL:
|
|
261
|
+
package_path = PackageSourceHandler.fetch_from_git_url(source, target_dir)
|
|
262
|
+
elif source_type == PackageSource.LOCAL:
|
|
263
|
+
source_path = Path(source)
|
|
264
|
+
package_path = PackageSourceHandler.fetch_from_local(
|
|
265
|
+
source_path, target_dir
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
raise InvalidSourceError(f"Unsupported source type: {source_type}")
|
|
269
|
+
|
|
270
|
+
return package_path, source_type
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""MCP protocol components."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["McpManager", "McpServerConfig"]
|
|
4
|
+
|
|
5
|
+
from uacs.protocols.mcp.manager import McpManager, McpServerConfig
|
|
6
|
+
|
|
7
|
+
# Note: McpToolAdapter (client utils) remains in multi-agent-cli
|
|
8
|
+
# as it depends on google-adk which is a MAOS dependency
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""MCP Server Manager."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class McpServerConfig:
|
|
10
|
+
"""Configuration for an MCP server."""
|
|
11
|
+
|
|
12
|
+
name: str
|
|
13
|
+
command: str
|
|
14
|
+
args: list[str]
|
|
15
|
+
env: dict[str, str]
|
|
16
|
+
enabled: bool = True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class McpManager:
|
|
20
|
+
"""Manages MCP server configurations."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config_dir: Path | None = None):
|
|
23
|
+
"""Initialize MCP manager.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config_dir: Directory to store configuration
|
|
27
|
+
"""
|
|
28
|
+
self.config_dir = config_dir or Path.home() / ".uacs"
|
|
29
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
self.config_file = self.config_dir / "mcp_servers.json"
|
|
31
|
+
self.servers: dict[str, McpServerConfig] = {}
|
|
32
|
+
self._load_config()
|
|
33
|
+
|
|
34
|
+
def _load_config(self):
|
|
35
|
+
"""Load configuration from file."""
|
|
36
|
+
if self.config_file.exists():
|
|
37
|
+
try:
|
|
38
|
+
data = json.loads(self.config_file.read_text())
|
|
39
|
+
for name, config in data.items():
|
|
40
|
+
self.servers[name] = McpServerConfig(**config)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
print(f"Error loading MCP config: {e}")
|
|
43
|
+
|
|
44
|
+
def _save_config(self):
|
|
45
|
+
"""Save configuration to file."""
|
|
46
|
+
data = {name: asdict(config) for name, config in self.servers.items()}
|
|
47
|
+
self.config_file.write_text(json.dumps(data, indent=2))
|
|
48
|
+
|
|
49
|
+
def add_server(
|
|
50
|
+
self,
|
|
51
|
+
name: str,
|
|
52
|
+
command: str,
|
|
53
|
+
args: list[str],
|
|
54
|
+
env: dict[str, str] | None = None,
|
|
55
|
+
):
|
|
56
|
+
"""Add or update an MCP server configuration."""
|
|
57
|
+
self.servers[name] = McpServerConfig(
|
|
58
|
+
name=name, command=command, args=args, env=env or {}
|
|
59
|
+
)
|
|
60
|
+
self._save_config()
|
|
61
|
+
|
|
62
|
+
def remove_server(self, name: str):
|
|
63
|
+
"""Remove an MCP server configuration."""
|
|
64
|
+
if name in self.servers:
|
|
65
|
+
del self.servers[name]
|
|
66
|
+
self._save_config()
|
|
67
|
+
|
|
68
|
+
def list_servers(self) -> list[McpServerConfig]:
|
|
69
|
+
"""List all configured servers."""
|
|
70
|
+
return list(self.servers.values())
|
|
71
|
+
|
|
72
|
+
def get_server(self, name: str) -> McpServerConfig | None:
|
|
73
|
+
"""Get a specific server configuration."""
|
|
74
|
+
return self.servers.get(name)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = ["McpManager", "McpServerConfig"]
|