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,367 @@
1
+ """Agent Skills SKILL.md validator based on agentskills.io specification.
2
+
3
+ Validates SKILL.md files against the official Agent Skills format specification:
4
+ - YAML frontmatter validation
5
+ - Required fields: name, description
6
+ - Allowed fields: name, description, license, allowed-tools, metadata, compatibility
7
+ - Name constraints: kebab-case, max 64 chars, no leading/trailing hyphens
8
+ - Description: max 1024 chars
9
+ - Compatibility: max 500 chars
10
+ - Directory name must match skill name
11
+ """
12
+
13
+ import re
14
+ import unicodedata
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Any, ClassVar
18
+
19
+ import yaml
20
+
21
+
22
+ @dataclass
23
+ class ValidationError:
24
+ """A single validation error."""
25
+
26
+ field: str
27
+ message: str
28
+ line: int | None = None
29
+
30
+
31
+ @dataclass
32
+ class ValidationResult:
33
+ """Result of validating a SKILL.md file."""
34
+
35
+ valid: bool
36
+ errors: list[ValidationError]
37
+ warnings: list[ValidationError]
38
+ metadata: dict[str, Any] | None = None
39
+
40
+
41
+ class SkillValidator:
42
+ """Validates Agent Skills SKILL.md files against specification.
43
+
44
+ See https://agentskills.io/specification for full format specification.
45
+ """
46
+
47
+ # Field constraints from spec
48
+ MAX_NAME_LENGTH: ClassVar[int] = 64
49
+ MAX_DESCRIPTION_LENGTH: ClassVar[int] = 1024
50
+ MAX_COMPATIBILITY_LENGTH: ClassVar[int] = 500
51
+
52
+ # Allowed frontmatter fields
53
+ ALLOWED_FIELDS: ClassVar[set[str]] = {
54
+ "name",
55
+ "description",
56
+ "license",
57
+ "allowed-tools",
58
+ "metadata",
59
+ "compatibility",
60
+ }
61
+
62
+ # Required fields
63
+ REQUIRED_FIELDS: ClassVar[set[str]] = {"name", "description"}
64
+
65
+ # Name pattern: lowercase letters, numbers, hyphens only
66
+ # Must not start or end with hyphen
67
+ NAME_PATTERN: ClassVar[re.Pattern] = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$")
68
+
69
+ @staticmethod
70
+ def normalize_unicode(text: str) -> str:
71
+ """Normalize Unicode text to NFC form."""
72
+ return unicodedata.normalize("NFC", text)
73
+
74
+ @staticmethod
75
+ def validate_name(name: str) -> list[ValidationError]:
76
+ """Validate skill name against spec constraints.
77
+
78
+ Constraints:
79
+ - Max 64 characters
80
+ - Lowercase letters, numbers, and hyphens only
81
+ - Must not start or end with a hyphen
82
+ - No consecutive hyphens
83
+ - Unicode normalized (NFC)
84
+ """
85
+ errors = []
86
+
87
+ if not name:
88
+ errors.append(ValidationError("name", "Name is required"))
89
+ return errors
90
+
91
+ # Normalize Unicode
92
+ normalized = SkillValidator.normalize_unicode(name)
93
+ if name != normalized:
94
+ errors.append(
95
+ ValidationError(
96
+ "name",
97
+ f"Name must be Unicode normalized (NFC). "
98
+ f"Got: {name!r}, expected: {normalized!r}",
99
+ )
100
+ )
101
+
102
+ # Length check
103
+ if len(name) > SkillValidator.MAX_NAME_LENGTH:
104
+ errors.append(
105
+ ValidationError(
106
+ "name",
107
+ f"Name exceeds maximum length of {SkillValidator.MAX_NAME_LENGTH} "
108
+ "characters",
109
+ )
110
+ )
111
+
112
+ # Pattern check (kebab-case)
113
+ if not SkillValidator.NAME_PATTERN.match(name):
114
+ errors.append(
115
+ ValidationError(
116
+ "name",
117
+ "Name must contain only lowercase letters, numbers, and hyphens. "
118
+ "Must not start or end with a hyphen.",
119
+ )
120
+ )
121
+
122
+ # Check for consecutive hyphens
123
+ if "--" in name:
124
+ errors.append(
125
+ ValidationError("name", "Name must not contain consecutive hyphens")
126
+ )
127
+
128
+ return errors
129
+
130
+ @staticmethod
131
+ def validate_description(description: str) -> list[ValidationError]:
132
+ """Validate skill description against spec constraints.
133
+
134
+ Constraints:
135
+ - Required
136
+ - Max 1024 characters
137
+ - Non-empty
138
+ """
139
+ errors = []
140
+
141
+ if not description:
142
+ errors.append(ValidationError("description", "Description is required"))
143
+ return errors
144
+
145
+ if not description.strip():
146
+ errors.append(
147
+ ValidationError("description", "Description must not be empty")
148
+ )
149
+
150
+ if len(description) > SkillValidator.MAX_DESCRIPTION_LENGTH:
151
+ errors.append(
152
+ ValidationError(
153
+ "description",
154
+ f"Description exceeds maximum length of "
155
+ f"{SkillValidator.MAX_DESCRIPTION_LENGTH} characters",
156
+ )
157
+ )
158
+
159
+ return errors
160
+
161
+ @staticmethod
162
+ def validate_compatibility(compatibility: str | None) -> list[ValidationError]:
163
+ """Validate compatibility field against spec constraints.
164
+
165
+ Constraints:
166
+ - Optional
167
+ - Max 500 characters
168
+ """
169
+ errors = []
170
+
171
+ if (
172
+ compatibility
173
+ and len(compatibility) > SkillValidator.MAX_COMPATIBILITY_LENGTH
174
+ ):
175
+ errors.append(
176
+ ValidationError(
177
+ "compatibility",
178
+ f"Compatibility exceeds maximum length of "
179
+ f"{SkillValidator.MAX_COMPATIBILITY_LENGTH} characters",
180
+ )
181
+ )
182
+
183
+ return errors
184
+
185
+ @staticmethod
186
+ def validate_frontmatter_fields(
187
+ frontmatter: dict[str, Any],
188
+ ) -> list[ValidationError]:
189
+ """Validate that only allowed fields are present in frontmatter."""
190
+ errors = []
191
+
192
+ extra_fields = set(frontmatter.keys()) - SkillValidator.ALLOWED_FIELDS
193
+ if extra_fields:
194
+ errors.append(
195
+ ValidationError(
196
+ "frontmatter",
197
+ f"Unexpected fields in frontmatter: "
198
+ f"{', '.join(sorted(extra_fields))}. "
199
+ f"Allowed fields: "
200
+ f"{', '.join(sorted(SkillValidator.ALLOWED_FIELDS))}",
201
+ )
202
+ )
203
+ missing_fields = SkillValidator.REQUIRED_FIELDS - set(frontmatter.keys())
204
+ if missing_fields:
205
+ errors.append(
206
+ ValidationError(
207
+ "frontmatter",
208
+ f"Missing required fields: {', '.join(sorted(missing_fields))}",
209
+ )
210
+ )
211
+
212
+ return errors
213
+
214
+ @staticmethod
215
+ def validate_directory_name(
216
+ skill_path: Path, skill_name: str
217
+ ) -> list[ValidationError]:
218
+ """Validate that directory name matches skill name.
219
+
220
+ Directory name must match the skill name from frontmatter (kebab-case).
221
+ """
222
+ errors = []
223
+
224
+ dir_name = skill_path.name
225
+ if dir_name != skill_name:
226
+ errors.append(
227
+ ValidationError(
228
+ "directory",
229
+ f"Directory name '{dir_name}' does not match skill name '{skill_name}'",
230
+ )
231
+ )
232
+
233
+ return errors
234
+
235
+ @staticmethod
236
+ def extract_frontmatter(
237
+ content: str,
238
+ ) -> tuple[dict[str, Any] | None, str | None, list[ValidationError]]:
239
+ """Extract and parse YAML frontmatter from SKILL.md content.
240
+
241
+ Returns:
242
+ Tuple of (parsed_frontmatter, remaining_content, errors)
243
+ """
244
+ errors = []
245
+
246
+ # Check for YAML frontmatter markers
247
+ if not content.startswith("---\n"):
248
+ errors.append(
249
+ ValidationError(
250
+ "frontmatter", "SKILL.md must start with YAML frontmatter (---)"
251
+ )
252
+ )
253
+ return None, content, errors
254
+
255
+ # Find end of frontmatter
256
+ lines = content.split("\n")
257
+ end_marker_idx = None
258
+ for i, line in enumerate(lines[1:], start=1):
259
+ if line.strip() == "---":
260
+ end_marker_idx = i
261
+ break
262
+
263
+ if end_marker_idx is None:
264
+ errors.append(
265
+ ValidationError(
266
+ "frontmatter",
267
+ "YAML frontmatter not properly closed (missing closing ---)",
268
+ )
269
+ )
270
+ return None, content, errors
271
+
272
+ # Extract frontmatter YAML
273
+ frontmatter_text = "\n".join(lines[1:end_marker_idx])
274
+
275
+ # Parse YAML
276
+ try:
277
+ frontmatter = yaml.safe_load(frontmatter_text)
278
+ if not isinstance(frontmatter, dict):
279
+ errors.append(
280
+ ValidationError(
281
+ "frontmatter",
282
+ "Frontmatter must be a YAML mapping (key-value pairs)",
283
+ )
284
+ )
285
+ return None, content, errors
286
+ except yaml.YAMLError as e:
287
+ errors.append(
288
+ ValidationError("frontmatter", f"Invalid YAML in frontmatter: {e}")
289
+ )
290
+ return None, content, errors
291
+
292
+ # Extract remaining content
293
+ remaining = "\n".join(lines[end_marker_idx + 1 :])
294
+
295
+ return frontmatter, remaining, errors
296
+
297
+ @staticmethod
298
+ def validate_file(skill_path: Path) -> ValidationResult:
299
+ """Validate a SKILL.md file at the given path.
300
+
301
+ Args:
302
+ skill_path: Path to directory containing SKILL.md
303
+
304
+ Returns:
305
+ ValidationResult with errors, warnings, and metadata
306
+ """
307
+ errors = []
308
+ warnings = []
309
+
310
+ # Check that SKILL.md exists
311
+ skill_file = skill_path / "SKILL.md"
312
+ if not skill_file.exists():
313
+ errors.append(
314
+ ValidationError("file", f"SKILL.md file not found in {skill_path}")
315
+ )
316
+ return ValidationResult(valid=False, errors=errors, warnings=warnings)
317
+
318
+ # Read file content
319
+ try:
320
+ content = skill_file.read_text(encoding="utf-8")
321
+ except Exception as e:
322
+ errors.append(ValidationError("file", f"Failed to read SKILL.md: {e}"))
323
+ return ValidationResult(valid=False, errors=errors, warnings=warnings)
324
+
325
+ # Extract and parse frontmatter
326
+ frontmatter, body, fm_errors = SkillValidator.extract_frontmatter(content)
327
+ errors.extend(fm_errors)
328
+
329
+ if not frontmatter:
330
+ return ValidationResult(valid=False, errors=errors, warnings=warnings)
331
+
332
+ # Validate frontmatter fields
333
+ errors.extend(SkillValidator.validate_frontmatter_fields(frontmatter))
334
+
335
+ # Validate individual fields
336
+ if "name" in frontmatter:
337
+ errors.extend(SkillValidator.validate_name(frontmatter["name"]))
338
+
339
+ # Validate directory name matches skill name
340
+ errors.extend(
341
+ SkillValidator.validate_directory_name(skill_path, frontmatter["name"])
342
+ )
343
+
344
+ if "description" in frontmatter:
345
+ errors.extend(
346
+ SkillValidator.validate_description(frontmatter["description"])
347
+ )
348
+
349
+ if "compatibility" in frontmatter:
350
+ errors.extend(
351
+ SkillValidator.validate_compatibility(frontmatter["compatibility"])
352
+ )
353
+
354
+ # Check for body content (warning if empty)
355
+ if body and not body.strip():
356
+ warnings.append(
357
+ ValidationError(
358
+ "body", "SKILL.md body is empty. Consider adding instructions."
359
+ )
360
+ )
361
+
362
+ return ValidationResult(
363
+ valid=len(errors) == 0,
364
+ errors=errors,
365
+ warnings=warnings,
366
+ metadata=frontmatter if len(errors) == 0 else None,
367
+ )
uacs/utils/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Utility functions for UACS."""
2
+
3
+ from uacs.utils.paths import get_project_root
4
+
5
+ __all__ = ["get_project_root"]
uacs/utils/paths.py ADDED
@@ -0,0 +1,24 @@
1
+ """Path utilities for UACS."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def get_project_root() -> Path:
8
+ """Get the effective project root directory.
9
+
10
+ Prioritizes PWD environment variable to handle cases where the tool
11
+ is invoked via 'uv run --directory ...' which changes the process CWD.
12
+
13
+ Returns:
14
+ Path to the project root directory
15
+ """
16
+ # Check if PWD is set and valid
17
+ pwd = os.environ.get("PWD")
18
+ if pwd:
19
+ path = Path(pwd)
20
+ if path.exists() and path.is_dir():
21
+ return path
22
+
23
+ # Fallback to current working directory
24
+ return Path.cwd()
@@ -0,0 +1,132 @@
1
+ # UACS Visualization Module
2
+
3
+ This module provides real-time web-based visualization of UACS context graphs.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ visualization/
9
+ ├── __init__.py # Module exports
10
+ ├── visualization.py # Terminal-based Rich visualizations
11
+ ├── web_server.py # FastAPI web server for browser UI
12
+ ├── static/ # Static web assets
13
+ │ └── index.html # Single-page web application
14
+ └── README.md # This file
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### From CLI
20
+
21
+ ```bash
22
+ # Start MCP server with web UI
23
+ uacs serve --with-ui
24
+
25
+ # Custom port
26
+ uacs serve --with-ui --ui-port 3000
27
+ ```
28
+
29
+ ### From Python
30
+
31
+ ```python
32
+ from pathlib import Path
33
+ from uacs.context.shared_context import SharedContextManager
34
+ from uacs.visualization.web_server import VisualizationServer
35
+ import uvicorn
36
+
37
+ # Initialize
38
+ manager = SharedContextManager(Path(".state/context"))
39
+ viz_server = VisualizationServer(manager, host="localhost", port=8081)
40
+
41
+ # Run server
42
+ config = uvicorn.Config(viz_server.app, host="localhost", port=8081)
43
+ server = uvicorn.Server(config)
44
+ await server.serve()
45
+ ```
46
+
47
+ ## Components
48
+
49
+ ### Terminal Visualization (`visualization.py`)
50
+
51
+ Rich-based terminal visualizations for CLI usage:
52
+ - Context graph as tree structure
53
+ - Token usage meters
54
+ - Agent interaction flow
55
+ - Live dashboard with auto-refresh
56
+
57
+ ### Web Visualization (`web_server.py`)
58
+
59
+ FastAPI server providing:
60
+ - REST API endpoints for context data
61
+ - WebSocket support for real-time updates
62
+ - Static file serving for web UI
63
+
64
+ ### Web UI (`static/index.html`)
65
+
66
+ Single-page application with:
67
+ - D3.js for interactive graphs
68
+ - Chart.js for statistics
69
+ - 5 visualization modes
70
+ - Real-time WebSocket updates
71
+
72
+ ## API Endpoints
73
+
74
+ | Endpoint | Description |
75
+ |----------|-------------|
76
+ | `GET /` | Main visualization page |
77
+ | `GET /api/graph` | Context graph data |
78
+ | `GET /api/stats` | Token statistics |
79
+ | `GET /api/topics` | Topic clusters |
80
+ | `GET /api/deduplication` | Deduplication data |
81
+ | `GET /api/quality` | Quality distribution |
82
+ | `WS /ws` | WebSocket for real-time updates |
83
+ | `GET /health` | Health check |
84
+
85
+ ## Visualization Modes
86
+
87
+ 1. **Conversation Flow** - Interactive D3.js force-directed graph
88
+ 2. **Token Dashboard** - Real-time token usage charts
89
+ 3. **Deduplication** - Duplicate content analysis
90
+ 4. **Quality Distribution** - Content quality metrics
91
+ 5. **Topic Clusters** - Topic network visualization
92
+
93
+ ## Documentation
94
+
95
+ For complete documentation, see: [docs/VISUALIZATION.md](../../../docs/VISUALIZATION.md)
96
+
97
+ ## Testing
98
+
99
+ ```bash
100
+ # Run tests
101
+ pytest tests/test_visualization_server.py -v
102
+
103
+ # Run demo
104
+ python examples/visualization_demo.py
105
+ ```
106
+
107
+ ## Development
108
+
109
+ ### Adding New Endpoints
110
+
111
+ 1. Add method to `VisualizationServer._setup_routes()`
112
+ 2. Implement data processing method
113
+ 3. Add frontend update function in `index.html`
114
+
115
+ ### Modifying Visualizations
116
+
117
+ Edit `static/index.html`:
118
+ - CSS for styling (in `<style>` section)
119
+ - JavaScript for behavior (in `<script>` section)
120
+ - D3.js/Chart.js configuration for visualizations
121
+
122
+ ## Dependencies
123
+
124
+ - FastAPI - Web framework
125
+ - Uvicorn - ASGI server
126
+ - WebSockets - Real-time communication
127
+ - D3.js (CDN) - Graph visualization
128
+ - Chart.js (CDN) - Statistical charts
129
+
130
+ ## License
131
+
132
+ MIT License (same as UACS)
@@ -0,0 +1,36 @@
1
+ """Visualization module for UACS context graphs and trace visualization."""
2
+
3
+ from uacs.visualization.visualization import ContextVisualizer
4
+ from uacs.visualization.web_server import VisualizationServer, start_visualization_server
5
+ from uacs.visualization.models import (
6
+ Event,
7
+ EventType,
8
+ Session,
9
+ SessionList,
10
+ EventList,
11
+ TokenAnalytics,
12
+ CompressionAnalytics,
13
+ TopicAnalytics,
14
+ SearchRequest,
15
+ SearchResults,
16
+ CompressionTrigger,
17
+ )
18
+ from uacs.visualization.storage import TraceStorage
19
+
20
+ __all__ = [
21
+ "ContextVisualizer",
22
+ "VisualizationServer",
23
+ "start_visualization_server",
24
+ "Event",
25
+ "EventType",
26
+ "Session",
27
+ "SessionList",
28
+ "EventList",
29
+ "TokenAnalytics",
30
+ "CompressionAnalytics",
31
+ "TopicAnalytics",
32
+ "SearchRequest",
33
+ "SearchResults",
34
+ "CompressionTrigger",
35
+ "TraceStorage",
36
+ ]