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,413 @@
1
+ """Package manager for installing and managing UACS skills.
2
+
3
+ Provides a minimal feature set similar to GitHub CLI extensions:
4
+ - Install packages from GitHub, Git URLs, or local paths
5
+ - List installed packages
6
+ - Validate packages
7
+ - Uninstall packages
8
+ - Update packages
9
+ """
10
+
11
+ import json
12
+ import re
13
+ import shutil
14
+ import tempfile
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from uacs.packages.models import InstalledPackage, PackageSource
19
+ from uacs.skills_validator import SkillValidator, ValidationResult
20
+
21
+
22
+ class PackageManagerError(Exception):
23
+ """Base exception for package manager errors."""
24
+
25
+ pass
26
+
27
+
28
+ class PackageSourceHandler:
29
+ """Handles parsing and fetching package sources."""
30
+
31
+ GITHUB_PATTERN = re.compile(r"^([a-zA-Z0-9_-]+)/([a-zA-Z0-9_.-]+)$")
32
+ GIT_URL_PATTERN = re.compile(r"^(https?://|git@|git://)")
33
+
34
+ @staticmethod
35
+ def parse_source(source: str) -> tuple[PackageSource, str]:
36
+ """Parse source string and determine type.
37
+
38
+ Args:
39
+ source: Source string (e.g., "owner/repo", "https://...", "/path/to/skill")
40
+
41
+ Returns:
42
+ Tuple of (PackageSource, normalized_source)
43
+ """
44
+ source = source.strip()
45
+
46
+ # Check if it's a GitHub shorthand (owner/repo)
47
+ if PackageSourceHandler.GITHUB_PATTERN.match(source):
48
+ return PackageSource.GITHUB, source
49
+
50
+ # Check if it's a Git URL
51
+ if PackageSourceHandler.GIT_URL_PATTERN.match(source):
52
+ return PackageSource.GIT_URL, source
53
+
54
+ # Check if it's a local path
55
+ path = Path(source).expanduser().resolve()
56
+ if path.exists():
57
+ return PackageSource.LOCAL, str(path)
58
+
59
+ return PackageSource.UNKNOWN, source
60
+
61
+ @staticmethod
62
+ def fetch(source: str, source_type: PackageSource, temp_dir: Path) -> Path:
63
+ """Fetch package source to temporary directory.
64
+
65
+ Args:
66
+ source: Source string
67
+ source_type: Type of source
68
+ temp_dir: Temporary directory to fetch to
69
+
70
+ Returns:
71
+ Path to fetched package directory
72
+ """
73
+ if source_type == PackageSource.LOCAL:
74
+ source_path = Path(source)
75
+ if not source_path.exists():
76
+ raise PackageManagerError(f"Local path does not exist: {source}")
77
+ # Copy local directory to temp
78
+ dest = temp_dir / source_path.name
79
+ shutil.copytree(source_path, dest, symlinks=False)
80
+ return dest
81
+
82
+ elif source_type == PackageSource.GITHUB:
83
+ # Convert GitHub shorthand to full URL
84
+ git_url = f"https://github.com/{source}.git"
85
+ return PackageSourceHandler._git_clone(git_url, temp_dir)
86
+
87
+ elif source_type == PackageSource.GIT_URL:
88
+ return PackageSourceHandler._git_clone(source, temp_dir)
89
+
90
+ else:
91
+ raise PackageManagerError(f"Unsupported source type: {source_type}")
92
+
93
+ @staticmethod
94
+ def _git_clone(url: str, temp_dir: Path) -> Path:
95
+ """Clone a git repository.
96
+
97
+ Args:
98
+ url: Git URL
99
+ temp_dir: Directory to clone into
100
+
101
+ Returns:
102
+ Path to cloned repository
103
+ """
104
+ import subprocess
105
+
106
+ # Extract repo name from URL
107
+ repo_name = url.rstrip("/").split("/")[-1]
108
+ if repo_name.endswith(".git"):
109
+ repo_name = repo_name[:-4]
110
+
111
+ dest = temp_dir / repo_name
112
+
113
+ try:
114
+ subprocess.run(
115
+ ["git", "clone", "--depth", "1", url, str(dest)],
116
+ check=True,
117
+ capture_output=True,
118
+ text=True,
119
+ )
120
+ except subprocess.CalledProcessError as e:
121
+ raise PackageManagerError(
122
+ f"Failed to clone repository: {e.stderr or e.stdout}"
123
+ )
124
+ except FileNotFoundError:
125
+ raise PackageManagerError(
126
+ "git command not found. Please install git to use git sources."
127
+ )
128
+
129
+ return dest
130
+
131
+
132
+ class PackageManager:
133
+ """Manages installation and lifecycle of UACS skill packages.
134
+
135
+ Packages are stored in .agent/skills/ directory with metadata in
136
+ .agent/skills/.packages.json.
137
+ """
138
+
139
+ def __init__(self, base_path: Path | None = None):
140
+ """Initialize package manager.
141
+
142
+ Args:
143
+ base_path: Base path for package storage. Defaults to current directory.
144
+ """
145
+ self.base_path = base_path or Path.cwd()
146
+ self.skills_dir = self.base_path / ".agent" / "skills"
147
+ self.metadata_file = self.skills_dir / ".packages.json"
148
+ self.validator = SkillValidator()
149
+
150
+ # Ensure directories exist
151
+ self.skills_dir.mkdir(parents=True, exist_ok=True)
152
+
153
+ def _load_metadata(self) -> dict[str, Any]:
154
+ """Load package metadata from .packages.json."""
155
+ if not self.metadata_file.exists():
156
+ return {"packages": {}}
157
+
158
+ try:
159
+ with open(self.metadata_file, "r", encoding="utf-8") as f:
160
+ return json.load(f)
161
+ except json.JSONDecodeError as e:
162
+ raise PackageManagerError(f"Failed to parse metadata file: {e}")
163
+
164
+ def _save_metadata(self, metadata: dict[str, Any]) -> None:
165
+ """Save package metadata to .packages.json."""
166
+ with open(self.metadata_file, "w", encoding="utf-8") as f:
167
+ json.dump(metadata, f, indent=2, ensure_ascii=False)
168
+
169
+ def install(self, source: str, validate: bool = True, force: bool = False) -> InstalledPackage:
170
+ """Install a package from a source.
171
+
172
+ Installation flow:
173
+ 1. Parse source to determine type
174
+ 2. Fetch to temporary directory
175
+ 3. Validate using SkillValidator (if validate=True)
176
+ 4. Copy to .agent/skills/{name}/
177
+ 5. Save metadata
178
+
179
+ Args:
180
+ source: Package source (GitHub repo, Git URL, or local path)
181
+ validate: Whether to validate before installing (default: True)
182
+ force: Whether to overwrite existing package (default: False)
183
+
184
+ Returns:
185
+ InstalledPackage with installation details
186
+
187
+ Raises:
188
+ PackageManagerError: If installation fails
189
+ """
190
+ # Parse source
191
+ source_type, normalized_source = PackageSourceHandler.parse_source(source)
192
+
193
+ if source_type == PackageSource.UNKNOWN:
194
+ raise PackageManagerError(
195
+ f"Unable to determine package source type: {source}\n"
196
+ "Supported formats:\n"
197
+ " - GitHub: owner/repo\n"
198
+ " - Git URL: https://... or git@...\n"
199
+ " - Local path: /path/to/skill"
200
+ )
201
+
202
+ # Create temporary directory for fetching
203
+ with tempfile.TemporaryDirectory() as temp_dir:
204
+ temp_path = Path(temp_dir)
205
+
206
+ # Fetch package to temp directory
207
+ try:
208
+ fetched_path = PackageSourceHandler.fetch(
209
+ normalized_source, source_type, temp_path
210
+ )
211
+ except Exception as e:
212
+ raise PackageManagerError(f"Failed to fetch package: {e}")
213
+
214
+ # Validate package if requested
215
+ if validate:
216
+ validation_result = self.validator.validate_file(fetched_path)
217
+
218
+ if not validation_result.valid:
219
+ error_messages = [
220
+ f"{err.field}: {err.message}" for err in validation_result.errors
221
+ ]
222
+ raise PackageManagerError(
223
+ f"Package validation failed:\n" + "\n".join(error_messages)
224
+ )
225
+
226
+ # Extract package name from metadata
227
+ if not validation_result.metadata or "name" not in validation_result.metadata:
228
+ raise PackageManagerError(
229
+ "Package validation succeeded but no name found in metadata"
230
+ )
231
+
232
+ package_name = validation_result.metadata["name"]
233
+ package_metadata = validation_result.metadata
234
+ else:
235
+ # Try to extract name from SKILL.md manually
236
+ skill_md = fetched_path / "SKILL.md"
237
+ if skill_md.exists():
238
+ import re
239
+ content = skill_md.read_text()
240
+ match = re.search(r'^name:\s*(.+)$', content, re.MULTILINE)
241
+ if match:
242
+ package_name = match.group(1).strip()
243
+ package_metadata = {"name": package_name}
244
+ else:
245
+ raise PackageManagerError(
246
+ "Could not determine package name. Enable validation or ensure SKILL.md has a 'name' field."
247
+ )
248
+ else:
249
+ raise PackageManagerError(
250
+ "No SKILL.md found. Enable validation to install this package."
251
+ )
252
+
253
+ # Check if already installed
254
+ metadata = self._load_metadata()
255
+ if package_name in metadata.get("packages", {}):
256
+ if not force:
257
+ raise PackageManagerError(
258
+ f"Package '{package_name}' is already installed. "
259
+ f"Use force=True to overwrite or update() to update it."
260
+ )
261
+ # Remove existing package if force=True
262
+ self.uninstall(package_name)
263
+
264
+ # Copy to skills directory
265
+ dest_path = self.skills_dir / package_name
266
+ if dest_path.exists():
267
+ raise PackageManagerError(
268
+ f"Directory already exists: {dest_path}. "
269
+ f"Remove it before installing."
270
+ )
271
+
272
+ try:
273
+ shutil.copytree(fetched_path, dest_path, symlinks=False)
274
+ except Exception as e:
275
+ raise PackageManagerError(f"Failed to copy package: {e}")
276
+
277
+ # Create installed package record
278
+ installed_pkg = InstalledPackage(
279
+ name=package_name,
280
+ source=source,
281
+ source_type=source_type,
282
+ version=package_metadata.get("version"),
283
+ location=dest_path,
284
+ is_valid=True,
285
+ validation_errors=[],
286
+ metadata=package_metadata,
287
+ )
288
+
289
+ # Save to metadata
290
+ metadata["packages"][package_name] = installed_pkg.to_dict()
291
+ self._save_metadata(metadata)
292
+
293
+ return installed_pkg
294
+
295
+ def list_installed(self) -> list[InstalledPackage]:
296
+ """List all installed packages.
297
+
298
+ Returns:
299
+ List of InstalledPackage objects
300
+ """
301
+ metadata = self._load_metadata()
302
+ packages = []
303
+
304
+ for pkg_data in metadata.get("packages", {}).values():
305
+ try:
306
+ packages.append(InstalledPackage.from_dict(pkg_data))
307
+ except Exception:
308
+ # Skip malformed entries
309
+ continue
310
+
311
+ return packages
312
+
313
+ def validate(self, package_name: str) -> ValidationResult:
314
+ """Validate an installed package.
315
+
316
+ Args:
317
+ package_name: Name of package to validate
318
+
319
+ Returns:
320
+ ValidationResult
321
+
322
+ Raises:
323
+ PackageManagerError: If package not found
324
+ """
325
+ metadata = self._load_metadata()
326
+ packages = metadata.get("packages", {})
327
+
328
+ if package_name not in packages:
329
+ raise PackageManagerError(f"Package not found: {package_name}")
330
+
331
+ pkg_data = packages[package_name]
332
+ location = Path(pkg_data["location"]) if pkg_data.get("location") else None
333
+
334
+ if not location or not location.exists():
335
+ raise PackageManagerError(
336
+ f"Package directory not found: {location}. Package may be corrupted."
337
+ )
338
+
339
+ return self.validator.validate_file(location)
340
+
341
+ def uninstall(self, package_name: str) -> bool:
342
+ """Uninstall a package.
343
+
344
+ Args:
345
+ package_name: Name of package to uninstall
346
+
347
+ Returns:
348
+ True if successful
349
+
350
+ Raises:
351
+ PackageManagerError: If uninstallation fails
352
+ """
353
+ metadata = self._load_metadata()
354
+ packages = metadata.get("packages", {})
355
+
356
+ if package_name not in packages:
357
+ raise PackageManagerError(f"Package not found: {package_name}")
358
+
359
+ # Get package location
360
+ pkg_data = packages[package_name]
361
+ location = Path(pkg_data["location"]) if pkg_data.get("location") else None
362
+
363
+ # Remove directory
364
+ if location and location.exists():
365
+ try:
366
+ shutil.rmtree(location)
367
+ except Exception as e:
368
+ raise PackageManagerError(
369
+ f"Failed to remove package directory: {e}"
370
+ )
371
+
372
+ # Remove from metadata
373
+ del packages[package_name]
374
+ self._save_metadata(metadata)
375
+
376
+ return True
377
+
378
+ def update(self, package_name: str) -> InstalledPackage:
379
+ """Update an installed package.
380
+
381
+ Args:
382
+ package_name: Name of package to update
383
+
384
+ Returns:
385
+ Updated InstalledPackage
386
+
387
+ Raises:
388
+ PackageManagerError: If update fails
389
+ """
390
+ metadata = self._load_metadata()
391
+ packages = metadata.get("packages", {})
392
+
393
+ if package_name not in packages:
394
+ raise PackageManagerError(f"Package not found: {package_name}")
395
+
396
+ pkg_data = packages[package_name]
397
+ source = pkg_data["source"]
398
+ source_type = PackageSource(pkg_data["source_type"])
399
+
400
+ # For local sources, we can't update
401
+ if source_type == PackageSource.LOCAL:
402
+ raise PackageManagerError(
403
+ "Cannot update local packages. Use uninstall and install instead."
404
+ )
405
+
406
+ # Uninstall existing
407
+ self.uninstall(package_name)
408
+
409
+ # Reinstall from source
410
+ try:
411
+ return self.install(source)
412
+ except Exception as e:
413
+ raise PackageManagerError(f"Failed to update package: {e}")
@@ -0,0 +1,60 @@
1
+ """Data models for package management."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ class PackageSource(Enum):
11
+ """Source type for package installation."""
12
+
13
+ GITHUB = "github" # owner/repo format
14
+ GIT_URL = "git_url" # Full git URL
15
+ LOCAL = "local" # Local filesystem path
16
+ UNKNOWN = "unknown"
17
+
18
+
19
+ @dataclass
20
+ class InstalledPackage:
21
+ """Represents an installed package in .agent/skills/"""
22
+
23
+ name: str
24
+ source: str # Original source string (e.g., "owner/repo")
25
+ source_type: PackageSource
26
+ version: str | None = None
27
+ install_date: datetime = field(default_factory=datetime.now)
28
+ location: Path | None = None
29
+ is_valid: bool = True
30
+ validation_errors: list[str] = field(default_factory=list)
31
+ metadata: dict[str, Any] = field(default_factory=dict)
32
+
33
+ def to_dict(self) -> dict[str, Any]:
34
+ """Convert to dictionary."""
35
+ return {
36
+ "name": self.name,
37
+ "source": self.source,
38
+ "source_type": self.source_type.value,
39
+ "version": self.version,
40
+ "install_date": self.install_date.isoformat(),
41
+ "location": str(self.location) if self.location else None,
42
+ "is_valid": self.is_valid,
43
+ "validation_errors": self.validation_errors,
44
+ "metadata": self.metadata,
45
+ }
46
+
47
+ @classmethod
48
+ def from_dict(cls, data: dict[str, Any]) -> "InstalledPackage":
49
+ """Create from dictionary."""
50
+ return cls(
51
+ name=data["name"],
52
+ source=data["source"],
53
+ source_type=PackageSource(data["source_type"]),
54
+ version=data.get("version"),
55
+ install_date=datetime.fromisoformat(data["install_date"]),
56
+ location=Path(data["location"]) if data.get("location") else None,
57
+ is_valid=data.get("is_valid", True),
58
+ validation_errors=data.get("validation_errors", []),
59
+ metadata=data.get("metadata", {}),
60
+ )