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
uacs/adapters/base.py ADDED
@@ -0,0 +1,261 @@
1
+ """Base adapter class for format translation."""
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 Skill:
11
+ """Skill information."""
12
+
13
+ name: str
14
+ instructions: str
15
+ triggers: list[str] = field(default_factory=list)
16
+ metadata: dict[str, Any] = field(default_factory=dict)
17
+ description: str | None = None
18
+ examples: list[str] = field(default_factory=list)
19
+
20
+
21
+ class ParsedContent:
22
+ """Parsed content from format."""
23
+
24
+ name: str | None = None
25
+ description: str | None = None
26
+ instructions: str | None = None
27
+ metadata: dict[str, Any] | None = None
28
+ rules: str | None = None
29
+
30
+ def __init__(self, **kwargs):
31
+ self.__dict__.update(kwargs)
32
+ # Set attributes from kwargs for type safety
33
+ for k, v in kwargs.items():
34
+ if hasattr(self, k):
35
+ setattr(self, k, v)
36
+
37
+ def to_dict(self) -> dict[str, Any]:
38
+ """Convert to dictionary."""
39
+ return self.__dict__
40
+
41
+ def __repr__(self) -> str:
42
+ return f"ParsedContent({self.__dict__})"
43
+
44
+
45
+ class BaseFormatAdapter(ABC):
46
+ """Base class for all format adapters."""
47
+
48
+ FORMAT_NAME: str = "base"
49
+ SUPPORTED_FILES: list[str] = []
50
+
51
+ def __init__(self, file_path: Path | None = None):
52
+ self.file_path = file_path
53
+ self.content = file_path.read_text() if file_path and file_path.exists() else ""
54
+ self.parsed = self.parse(self.content) if self.content else None
55
+
56
+ @abstractmethod
57
+ def parse(self, content: str) -> ParsedContent:
58
+ """Parse format-specific content.
59
+
60
+ Args:
61
+ content: Raw file content
62
+
63
+ Returns:
64
+ Parsed content object
65
+ """
66
+ pass
67
+
68
+ @abstractmethod
69
+ def to_system_prompt(self) -> str:
70
+ """Convert to system prompt format.
71
+
72
+ Returns:
73
+ Formatted system prompt string
74
+ """
75
+ pass
76
+
77
+ def to_adk_capabilities(self) -> dict[str, Any]:
78
+ """Convert to ADK agent card format (optional).
79
+
80
+ Returns:
81
+ ADK capabilities dictionary
82
+ """
83
+ return {}
84
+
85
+ def exists(self) -> bool:
86
+ """Check if file exists.
87
+
88
+ Returns:
89
+ True if file exists
90
+ """
91
+ return self.file_path and self.file_path.exists()
92
+
93
+ def get_stats(self) -> dict[str, Any]:
94
+ """Get adapter statistics.
95
+
96
+ Returns:
97
+ Dictionary with adapter stats
98
+ """
99
+ return {
100
+ "format": self.FORMAT_NAME,
101
+ "file": str(self.file_path),
102
+ "exists": self.exists(),
103
+ "size": len(self.content),
104
+ "parsed": self.parsed.to_dict() if self.parsed else None,
105
+ }
106
+
107
+ def find_skill_by_trigger(self, query: str) -> Skill | None:
108
+ """Find skill that matches query trigger.
109
+
110
+ Default implementation: no matching.
111
+ Subclasses can override for trigger-based matching.
112
+
113
+ Args:
114
+ query: User query to match against triggers
115
+
116
+ Returns:
117
+ Matching skill or None
118
+ """
119
+ return None
120
+
121
+ @classmethod
122
+ def supports_file(cls, file_path: Path) -> bool:
123
+ """Check if this adapter supports the file.
124
+
125
+ Args:
126
+ file_path: Path to check
127
+
128
+ Returns:
129
+ True if adapter supports this file
130
+ """
131
+ return file_path.name in cls.SUPPORTED_FILES
132
+
133
+
134
+ class FormatAdapterRegistry:
135
+ """Registry for format adapters."""
136
+
137
+ _adapters: dict[str, type["BaseFormatAdapter"]] = {}
138
+
139
+ @classmethod
140
+ def register(
141
+ cls, adapter_class: type["BaseFormatAdapter"]
142
+ ) -> type["BaseFormatAdapter"]:
143
+ """Register adapter class.
144
+
145
+ Args:
146
+ adapter_class: Adapter class to register
147
+
148
+ Returns:
149
+ The adapter class (for decorator usage)
150
+ """
151
+ cls._adapters[adapter_class.FORMAT_NAME] = adapter_class
152
+ return adapter_class
153
+
154
+ @classmethod
155
+ def detect_and_load(
156
+ cls, project_path: Path, search_parents: bool = True
157
+ ) -> BaseFormatAdapter | None:
158
+ """Auto-detect format and return adapter.
159
+
160
+ Searches for supported files in:
161
+ 1. Project directory (project-specific config)
162
+ 2. Parent directories up to root (following AGENTS.md spec)
163
+ 3. User home directory (personal/global config)
164
+
165
+ Args:
166
+ project_path: Path to project directory
167
+ search_parents: Whether to search parent directories
168
+
169
+ Returns:
170
+ Adapter instance or None if no format detected
171
+ """
172
+ search_paths = [project_path]
173
+
174
+ # Add parent directories
175
+ if search_parents:
176
+ current = project_path
177
+ while current != current.parent:
178
+ current = current.parent
179
+ search_paths.append(current)
180
+
181
+ # Add user home directory for personal skills
182
+ home_config_paths = [
183
+ Path.home() / ".claude", # Claude Code personal skills
184
+ Path.home() / ".config" / "uacs", # UACS config directory
185
+ ]
186
+ search_paths.extend(home_config_paths)
187
+
188
+ # Search all paths for supported files
189
+ for adapter_class in cls._adapters.values():
190
+ for filename in adapter_class.SUPPORTED_FILES:
191
+ for search_path in search_paths:
192
+ file_path = search_path / filename
193
+ if file_path.exists():
194
+ return adapter_class(file_path)
195
+
196
+ return None
197
+
198
+ @classmethod
199
+ def detect_and_load_all(cls, project_path: Path) -> list[BaseFormatAdapter]:
200
+ """Detect and load all available format adapters.
201
+
202
+ Finds all supported files in project and personal directories.
203
+ Also discovers multiple SKILL.md files from .claude/skills/*
204
+
205
+ Args:
206
+ project_path: Path to project directory
207
+
208
+ Returns:
209
+ List of adapter instances
210
+ """
211
+ adapters = []
212
+ found_files = set()
213
+
214
+ search_paths = [
215
+ project_path,
216
+ Path.home() / ".claude",
217
+ Path.home() / ".config" / "uacs",
218
+ ]
219
+
220
+ for adapter_class in cls._adapters.values():
221
+ for filename in adapter_class.SUPPORTED_FILES:
222
+ for search_path in search_paths:
223
+ file_path = search_path / filename
224
+ if file_path.exists() and str(file_path) not in found_files:
225
+ adapters.append(adapter_class(file_path))
226
+ found_files.add(str(file_path))
227
+
228
+ # Special case: Discover multiple SKILL.md files from .agent/skills/* and .claude/skills/*
229
+ from .agent_skill_adapter import AgentSkillAdapter
230
+
231
+ agent_skills = AgentSkillAdapter.discover_skills(project_path)
232
+ for skill in agent_skills:
233
+ if str(skill.file_path) not in found_files:
234
+ adapters.append(skill)
235
+ found_files.add(str(skill.file_path))
236
+
237
+ return adapters
238
+
239
+ @classmethod
240
+ def list_formats(cls) -> list[str]:
241
+ """List registered formats.
242
+
243
+ Returns:
244
+ List of format names
245
+ """
246
+ return list(cls._adapters.keys())
247
+
248
+ @classmethod
249
+ def get_adapter(cls, format_name: str) -> type | None:
250
+ """Get adapter class by format name.
251
+
252
+ Args:
253
+ format_name: Name of the format
254
+
255
+ Returns:
256
+ Adapter class or None if not found
257
+ """
258
+ return cls._adapters.get(format_name)
259
+
260
+
261
+ __all__ = ["BaseFormatAdapter", "FormatAdapterRegistry", "ParsedContent", "Skill"]
@@ -0,0 +1,39 @@
1
+ """Adapter for Cline .clinerules format."""
2
+
3
+ from .base import BaseFormatAdapter, FormatAdapterRegistry, ParsedContent
4
+
5
+
6
+ @FormatAdapterRegistry.register
7
+ class ClineRulesAdapter(BaseFormatAdapter):
8
+ """Cline .clinerules format adapter.
9
+
10
+ Cline rules are typically plain text instructions for the Cline AI assistant.
11
+ """
12
+
13
+ FORMAT_NAME = "clinerules"
14
+ SUPPORTED_FILES = [".clinerules"]
15
+
16
+ def parse(self, content: str) -> ParsedContent:
17
+ """Parse .clinerules format.
18
+
19
+ Args:
20
+ content: Raw .clinerules content
21
+
22
+ Returns:
23
+ ParsedContent with rules
24
+ """
25
+ return ParsedContent(rules=content.strip())
26
+
27
+ def to_system_prompt(self) -> str:
28
+ """Convert to system prompt.
29
+
30
+ Returns:
31
+ Formatted system prompt
32
+ """
33
+ if not self.parsed or not self.parsed.rules:
34
+ return ""
35
+
36
+ return f"# PROJECT RULES\n\n{self.parsed.rules}"
37
+
38
+
39
+ __all__ = ["ClineRulesAdapter"]
@@ -0,0 +1,39 @@
1
+ """Adapter for Cursor .cursorrules format."""
2
+
3
+ from .base import BaseFormatAdapter, FormatAdapterRegistry, ParsedContent
4
+
5
+
6
+ @FormatAdapterRegistry.register
7
+ class CursorRulesAdapter(BaseFormatAdapter):
8
+ """Cursor .cursorrules format adapter.
9
+
10
+ Cursor rules are typically plain text instructions for the Cursor editor.
11
+ """
12
+
13
+ FORMAT_NAME = "cursorrules"
14
+ SUPPORTED_FILES = [".cursorrules"]
15
+
16
+ def parse(self, content: str) -> ParsedContent:
17
+ """Parse .cursorrules format (usually plain text).
18
+
19
+ Args:
20
+ content: Raw .cursorrules content
21
+
22
+ Returns:
23
+ ParsedContent with rules
24
+ """
25
+ return ParsedContent(rules=content.strip())
26
+
27
+ def to_system_prompt(self) -> str:
28
+ """Convert to system prompt.
29
+
30
+ Returns:
31
+ Formatted system prompt
32
+ """
33
+ if not self.parsed or not self.parsed.rules:
34
+ return ""
35
+
36
+ return f"# PROJECT RULES\n\n{self.parsed.rules}"
37
+
38
+
39
+ __all__ = ["CursorRulesAdapter"]
uacs/api.py ADDED
@@ -0,0 +1,262 @@
1
+ """Universal Agent Context System - Main API
2
+
3
+ See docs/uacs/README.md for details.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from uacs.adapters.agent_skill_adapter import AgentSkillAdapter
10
+ from uacs.adapters.agents_md_adapter import AgentsMDAdapter
11
+ from uacs.context.shared_context import SharedContextManager
12
+ from uacs.context.unified_context import UnifiedContextAdapter
13
+ from uacs.packages import PackageManager
14
+
15
+
16
+ class UACS:
17
+ """Universal Agent Context System
18
+
19
+ Provides unified context management across:
20
+ - Packages (Local package management)
21
+ - Adapters (Format translation)
22
+ - Context (Shared memory + compression)
23
+ - Project metadata (AGENTS.md)
24
+
25
+ Example:
26
+ >>> uacs = UACS(project_path=Path("."))
27
+ >>> uacs.install_package("owner/repo")
28
+ >>> context = uacs.build_context(query="...", agent="claude")
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ project_path: Path,
34
+ ):
35
+ """Initialize UACS.
36
+
37
+ Args:
38
+ project_path: Path to the project root
39
+ """
40
+ self.project_path = project_path
41
+
42
+ # Initialize components
43
+ self.packages = PackageManager(project_path)
44
+
45
+ # Detect and load project format adapters
46
+ agents_md_path = project_path / "AGENTS.md"
47
+
48
+ # Modern Agent Skills support (.agent/skills/)
49
+ self.agent_skills = AgentSkillAdapter.discover_skills(project_path)
50
+
51
+ self.agents_md = (
52
+ AgentsMDAdapter(agents_md_path) if agents_md_path.exists() else None
53
+ )
54
+
55
+ # Initialize shared context and unified adapter
56
+ self.shared_context = SharedContextManager(project_path / ".state" / "context")
57
+ self.unified_context = UnifiedContextAdapter(
58
+ agents_md_path=agents_md_path if agents_md_path.exists() else None,
59
+ context_storage=project_path / ".state" / "context",
60
+ )
61
+
62
+ def install_package(
63
+ self,
64
+ source: str,
65
+ validate: bool = True,
66
+ force: bool = False,
67
+ ) -> Any:
68
+ """Install a package from GitHub, Git URL, or local path.
69
+
70
+ Args:
71
+ source: Package source (owner/repo, git URL, or local path)
72
+ validate: Whether to validate before installing
73
+ force: Whether to overwrite existing package
74
+
75
+ Returns:
76
+ Installed package information
77
+ """
78
+ return self.packages.install(source, validate=validate, force=force)
79
+
80
+ def list_packages(self) -> list[Any]:
81
+ """List installed packages.
82
+
83
+ Returns:
84
+ List of installed packages
85
+ """
86
+ return self.packages.list_installed()
87
+
88
+ def get_capabilities(self, agent: str | None = None) -> dict[str, Any]:
89
+ """Get available capabilities for an agent.
90
+
91
+ Args:
92
+ agent: Optional agent name to filter capabilities
93
+
94
+ Returns:
95
+ Dictionary of capabilities
96
+ """
97
+ capabilities = self.unified_context.get_capabilities(agent_name=agent)
98
+
99
+ # Add discovered Agent Skills
100
+ if self.agent_skills:
101
+ capabilities["agent_skills"] = [
102
+ {
103
+ "name": skill.parsed.name,
104
+ "description": skill.parsed.description,
105
+ "triggers": skill.parsed.triggers,
106
+ "source": skill.source_directory or "local",
107
+ }
108
+ for skill in self.agent_skills
109
+ if skill.parsed
110
+ ]
111
+
112
+ return capabilities
113
+
114
+ def build_context(
115
+ self,
116
+ query: str,
117
+ agent: str,
118
+ max_tokens: int | None = None,
119
+ topics: list[str] | None = None,
120
+ ) -> str:
121
+ """Build context for an agent query.
122
+
123
+ Args:
124
+ query: The query or task
125
+ agent: Agent name (claude, gemini, etc.)
126
+ max_tokens: Optional token limit
127
+ topics: Optional topics to filter and prioritize context
128
+
129
+ Returns:
130
+ Formatted context string
131
+ """
132
+ # If topics are provided, use focused context
133
+ if topics:
134
+ focused_context = self.shared_context.get_focused_context(
135
+ topics=topics, agent=agent, max_tokens=max_tokens or 4000
136
+ )
137
+ # Combine with unified context
138
+ unified = self.unified_context.build_context(
139
+ query=query, agent_name=agent, max_tokens=max_tokens
140
+ )
141
+ return f"{focused_context}\n\n{unified}" if focused_context else unified
142
+
143
+ return self.unified_context.build_context(
144
+ query=query, agent_name=agent, max_tokens=max_tokens
145
+ )
146
+
147
+ def add_to_context(
148
+ self,
149
+ key: str,
150
+ content: str,
151
+ metadata: dict[str, Any] | None = None,
152
+ topics: list[str] | None = None,
153
+ ):
154
+ """Add content to shared context.
155
+
156
+ Args:
157
+ key: Context key (used as agent name)
158
+ content: Content to store
159
+ metadata: Optional metadata
160
+ topics: Optional topics for semantic filtering
161
+
162
+ Example:
163
+ uacs.add_to_context(
164
+ "claude",
165
+ "Reviewed auth.py, found SQL injection",
166
+ topics=["code-review", "security"]
167
+ )
168
+ """
169
+ # Add to shared context with topics
170
+ self.shared_context.add_entry(
171
+ content=content,
172
+ agent=key,
173
+ metadata=metadata,
174
+ topics=topics,
175
+ )
176
+
177
+ def get_token_stats(self) -> dict[str, int]:
178
+ """Get token usage statistics.
179
+
180
+ Returns:
181
+ Dictionary of token counts
182
+ """
183
+ return self.unified_context.get_token_stats()
184
+
185
+ def export_config(self, output_path: Path):
186
+ """Export UACS configuration.
187
+
188
+ Args:
189
+ output_path: Path to save configuration
190
+ """
191
+ self.unified_context.export_unified_config(output_path)
192
+
193
+
194
+ def visualize_context(self, output_path: Path | None = None) -> str:
195
+ """Visualize context structure.
196
+
197
+ Args:
198
+ output_path: Optional path to save visualization
199
+
200
+ Returns:
201
+ Visualization string (ASCII art or HTML)
202
+ """
203
+ from uacs.visualization import ContextVisualizer
204
+
205
+ visualizer = ContextVisualizer()
206
+ # Get graph and stats from shared context
207
+ graph = self.shared_context.get_context_graph()
208
+ stats = self.shared_context.get_stats()
209
+
210
+ # Render the visualizations
211
+ graph_panel = visualizer.render_context_graph(graph)
212
+ stats_table = visualizer.render_stats_table(stats)
213
+
214
+ # Print to console
215
+ visualizer.console.print(graph_panel)
216
+ visualizer.console.print(stats_table)
217
+
218
+ # Optionally save to file
219
+ if output_path:
220
+ output_path.write_text(f"Context Graph: {graph}\nStats: {stats}")
221
+ return f"Visualization saved to {output_path}"
222
+
223
+ return "Visualization rendered to console"
224
+
225
+
226
+ def get_stats(self) -> dict[str, Any]:
227
+ """Get comprehensive UACS statistics.
228
+
229
+ Returns:
230
+ Dictionary with statistics from all components
231
+ """
232
+ # Merge token stats with shared context stats
233
+ token_stats = self.get_token_stats()
234
+ context_stats = self.shared_context.get_stats()
235
+ token_stats.update(context_stats)
236
+
237
+ stats = {
238
+ "project_path": str(self.project_path),
239
+ "adapters": {
240
+ "agent_skills": {
241
+ "count": len(self.agent_skills),
242
+ "paths": [str(s.file_path) for s in self.agent_skills],
243
+ },
244
+ "agents_md": {
245
+ "loaded": self.agents_md is not None,
246
+ "path": str(self.project_path / "AGENTS.md")
247
+ if self.agents_md
248
+ else None,
249
+ },
250
+ },
251
+ "packages": {
252
+ "installed_count": len(self.packages.list_installed()),
253
+ "skills_dir": str(self.project_path / ".agent" / "skills"),
254
+ },
255
+ "context": token_stats,
256
+ "capabilities": self.get_capabilities(),
257
+ }
258
+
259
+ return stats
260
+
261
+
262
+ __all__ = ["UACS"]
uacs/cli/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """CLI commands for UACS."""
2
+
3
+ from uacs.cli import context, memory, mcp, packages, skills
4
+ from uacs.cli.main import app
5
+
6
+ __all__ = ["app", "context", "skills", "packages", "memory", "mcp"]