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.
Files changed (47) hide show
  1. uacs/__init__.py +12 -0
  2. uacs/adapters/__init__.py +19 -0
  3. uacs/adapters/agent_skill_adapter.py +202 -0
  4. uacs/adapters/agents_md_adapter.py +330 -0
  5. uacs/adapters/base.py +261 -0
  6. uacs/adapters/clinerules_adapter.py +39 -0
  7. uacs/adapters/cursorrules_adapter.py +39 -0
  8. uacs/api.py +262 -0
  9. uacs/cli/__init__.py +6 -0
  10. uacs/cli/context.py +349 -0
  11. uacs/cli/main.py +195 -0
  12. uacs/cli/mcp.py +115 -0
  13. uacs/cli/memory.py +142 -0
  14. uacs/cli/packages.py +309 -0
  15. uacs/cli/skills.py +144 -0
  16. uacs/cli/utils.py +24 -0
  17. uacs/config/repositories.yaml +26 -0
  18. uacs/context/__init__.py +0 -0
  19. uacs/context/agent_context.py +406 -0
  20. uacs/context/shared_context.py +661 -0
  21. uacs/context/unified_context.py +332 -0
  22. uacs/mcp_server_entry.py +80 -0
  23. uacs/memory/__init__.py +5 -0
  24. uacs/memory/simple_memory.py +255 -0
  25. uacs/packages/__init__.py +26 -0
  26. uacs/packages/manager.py +413 -0
  27. uacs/packages/models.py +60 -0
  28. uacs/packages/sources.py +270 -0
  29. uacs/protocols/__init__.py +5 -0
  30. uacs/protocols/mcp/__init__.py +8 -0
  31. uacs/protocols/mcp/manager.py +77 -0
  32. uacs/protocols/mcp/skills_server.py +700 -0
  33. uacs/skills_validator.py +367 -0
  34. uacs/utils/__init__.py +5 -0
  35. uacs/utils/paths.py +24 -0
  36. uacs/visualization/README.md +132 -0
  37. uacs/visualization/__init__.py +36 -0
  38. uacs/visualization/models.py +195 -0
  39. uacs/visualization/static/index.html +857 -0
  40. uacs/visualization/storage.py +402 -0
  41. uacs/visualization/visualization.py +328 -0
  42. uacs/visualization/web_server.py +364 -0
  43. universal_agent_context-0.2.0.dist-info/METADATA +873 -0
  44. universal_agent_context-0.2.0.dist-info/RECORD +47 -0
  45. universal_agent_context-0.2.0.dist-info/WHEEL +4 -0
  46. universal_agent_context-0.2.0.dist-info/entry_points.txt +2 -0
  47. universal_agent_context-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -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,5 @@
1
+ """MCP protocol implementation."""
2
+
3
+ __all__ = ["McpManager", "McpServerConfig"]
4
+
5
+ from uacs.protocols.mcp.manager import McpManager, McpServerConfig
@@ -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"]