Initial commit
This commit is contained in:
431
skills/registry.query/SKILL.md
Normal file
431
skills/registry.query/SKILL.md
Normal file
@@ -0,0 +1,431 @@
|
||||
# registry.query
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Status:** Active
|
||||
**Tags:** registry, search, query, discovery, metadata, cli
|
||||
|
||||
## Overview
|
||||
|
||||
The `registry.query` skill enables programmatic searching of Betty registries (skills, agents, and commands) with flexible filtering capabilities. It's designed for dynamic discovery, workflow automation, and CLI autocompletion.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-Registry Support**: Query skills, agents, commands, or hooks registries
|
||||
- **Flexible Filtering**: Filter by name, version, status, tags, domain, and capability
|
||||
- **Fuzzy Matching**: Optional fuzzy search for name and capability fields
|
||||
- **Result Limiting**: Control the number of results returned
|
||||
- **Rich Metadata**: Returns key metadata for each matching entry
|
||||
- **Multiple Output Formats**: JSON, table, or compact format for different use cases
|
||||
- **Table Formatting**: Aligned column display for easy CLI viewing
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line
|
||||
|
||||
```bash
|
||||
# List all skills (compact format, default)
|
||||
python3 skills/registry.query/registry_query.py skills
|
||||
|
||||
# Find skills with 'api' tag in table format
|
||||
python3 skills/registry.query/registry_query.py skills --tag api --format table
|
||||
|
||||
# Find agents with 'design' capability
|
||||
python3 skills/registry.query/registry_query.py agents --capability design
|
||||
|
||||
# Query hooks registry
|
||||
python3 skills/registry.query/registry_query.py hooks --status active --format table
|
||||
|
||||
# Find active skills with name containing 'validate'
|
||||
python3 skills/registry.query/registry_query.py skills --name validate --status active
|
||||
|
||||
# Fuzzy search for commands
|
||||
python3 skills/registry.query/registry_query.py commands --name test --fuzzy
|
||||
|
||||
# Limit results to top 5
|
||||
python3 skills/registry.query/registry_query.py skills --tag api --limit 5
|
||||
|
||||
# Get full JSON output
|
||||
python3 skills/registry.query/registry_query.py skills --tag validation --format json
|
||||
```
|
||||
|
||||
### Programmatic Use
|
||||
|
||||
```python
|
||||
from skills.registry.query.registry_query import query_registry
|
||||
|
||||
# Query skills with API tag
|
||||
result = query_registry(
|
||||
registry="skills",
|
||||
tag="api",
|
||||
status="active"
|
||||
)
|
||||
|
||||
if result["ok"]:
|
||||
matching_entries = result["details"]["results"]
|
||||
for entry in matching_entries:
|
||||
print(f"{entry['name']}: {entry['description']}")
|
||||
```
|
||||
|
||||
### Betty CLI
|
||||
|
||||
```bash
|
||||
# Via Betty CLI (when registered)
|
||||
betty registry query skills --tag api
|
||||
betty registry query agents --capability "API design"
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
### Required
|
||||
|
||||
- **`registry`** (string): Registry to query
|
||||
- Valid values: `skills`, `agents`, `commands`, `hooks`
|
||||
|
||||
### Optional Filters
|
||||
|
||||
- **`name`** (string): Filter by name (substring match, case-insensitive)
|
||||
- **`version`** (string): Filter by exact version match
|
||||
- **`status`** (string): Filter by status (e.g., `active`, `draft`, `deprecated`, `archived`)
|
||||
- **`tag`** (string): Filter by single tag
|
||||
- **`tags`** (array): Filter by multiple tags (matches any)
|
||||
- **`capability`** (string): Filter by capability (agents only, substring match)
|
||||
- **`domain`** (string): Filter by domain (alias for tag filter)
|
||||
- **`fuzzy`** (boolean): Enable fuzzy matching for name and capability
|
||||
- **`limit`** (integer): Maximum number of results to return
|
||||
- **`format`** (string): Output format (`json`, `table`, `compact`)
|
||||
- `json`: Full JSON response with all metadata
|
||||
- `table`: Aligned column table for easy reading
|
||||
- `compact`: Detailed list format (default)
|
||||
|
||||
## Output Format
|
||||
|
||||
### Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"status": "success",
|
||||
"errors": [],
|
||||
"timestamp": "2025-10-23T10:30:00.000000Z",
|
||||
"details": {
|
||||
"registry": "skills",
|
||||
"query": {
|
||||
"name": "api",
|
||||
"version": null,
|
||||
"status": "active",
|
||||
"tags": ["validation"],
|
||||
"capability": null,
|
||||
"domain": null,
|
||||
"fuzzy": false,
|
||||
"limit": null
|
||||
},
|
||||
"total_entries": 21,
|
||||
"matching_entries": 3,
|
||||
"results": [
|
||||
{
|
||||
"name": "api.validate",
|
||||
"version": "0.1.0",
|
||||
"description": "Validates OpenAPI or AsyncAPI specifications...",
|
||||
"status": "active",
|
||||
"tags": ["api", "validation", "openapi", "asyncapi"],
|
||||
"dependencies": ["context.schema"],
|
||||
"entrypoints": [
|
||||
{
|
||||
"command": "/api/validate",
|
||||
"runtime": "python",
|
||||
"description": "Validate API specification files"
|
||||
}
|
||||
],
|
||||
"inputs": [...],
|
||||
"outputs": [...]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"status": "failed",
|
||||
"errors": ["Invalid registry: invalid_type"],
|
||||
"timestamp": "2025-10-23T10:30:00.000000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Metadata Fields by Registry Type
|
||||
|
||||
### Skills
|
||||
|
||||
- `name`, `version`, `description`, `status`, `tags`
|
||||
- `dependencies`: List of required skills
|
||||
- `entrypoints`: Available commands and handlers
|
||||
- `inputs`: Expected input parameters
|
||||
- `outputs`: Generated outputs
|
||||
|
||||
### Agents
|
||||
|
||||
- `name`, `version`, `description`, `status`, `tags`
|
||||
- `capabilities`: List of agent capabilities
|
||||
- `skills_available`: Skills the agent can invoke
|
||||
- `reasoning_mode`: `oneshot` or `iterative`
|
||||
- `context_requirements`: Required context fields
|
||||
|
||||
### Commands
|
||||
|
||||
- `name`, `version`, `description`, `status`, `tags`
|
||||
- `execution`: Execution configuration (type, target)
|
||||
- `parameters`: Command parameters
|
||||
|
||||
### Hooks
|
||||
|
||||
- `name`, `version`, `description`, `status`, `tags`
|
||||
- `event`: Hook event trigger (e.g., on_file_edit, on_commit)
|
||||
- `command`: Command to execute
|
||||
- `enabled`: Whether the hook is enabled
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Dynamic Discovery
|
||||
|
||||
Find skills related to a specific domain:
|
||||
|
||||
```bash
|
||||
python3 skills/registry.query/registry_query.py skills --domain api
|
||||
```
|
||||
|
||||
### 2. Workflow Automation
|
||||
|
||||
Programmatically find and invoke skills:
|
||||
|
||||
```python
|
||||
# Find validation skills
|
||||
result = query_registry(registry="skills", tag="validation", status="active")
|
||||
|
||||
for skill in result["details"]["results"]:
|
||||
print(f"Found validation skill: {skill['name']}")
|
||||
# Invoke skill programmatically
|
||||
```
|
||||
|
||||
### 3. CLI Autocompletion
|
||||
|
||||
Generate autocompletion data:
|
||||
|
||||
```python
|
||||
# Get all active skill names for tab completion
|
||||
result = query_registry(registry="skills", status="active")
|
||||
skill_names = [s["name"] for s in result["details"]["results"]]
|
||||
```
|
||||
|
||||
### 4. Dependency Resolution
|
||||
|
||||
Find skills with specific dependencies:
|
||||
|
||||
```python
|
||||
result = query_registry(registry="skills", status="active")
|
||||
for skill in result["details"]["results"]:
|
||||
if "context.schema" in skill.get("dependencies", []):
|
||||
print(f"{skill['name']} depends on context.schema")
|
||||
```
|
||||
|
||||
### 5. Capability Search
|
||||
|
||||
Find agents by capability:
|
||||
|
||||
```bash
|
||||
python3 skills/registry.query/registry_query.py agents --capability "API design"
|
||||
```
|
||||
|
||||
### 6. Hooks Management
|
||||
|
||||
Query and monitor hooks:
|
||||
|
||||
```bash
|
||||
# List all hooks in table format
|
||||
python3 skills/registry.query/registry_query.py hooks --format table
|
||||
|
||||
# Find hooks by event type
|
||||
python3 skills/registry.query/registry_query.py hooks --tag commit
|
||||
|
||||
# Find enabled hooks
|
||||
python3 skills/registry.query/registry_query.py hooks --status active
|
||||
```
|
||||
|
||||
### 7. Status Monitoring
|
||||
|
||||
Find deprecated or draft entries:
|
||||
|
||||
```bash
|
||||
python3 skills/registry.query/registry_query.py skills --status deprecated
|
||||
python3 skills/registry.query/registry_query.py skills --status draft
|
||||
```
|
||||
|
||||
## Future Extensions
|
||||
|
||||
The skill is designed with these future enhancements in mind:
|
||||
|
||||
1. **Advanced Fuzzy Matching**: Implement more sophisticated fuzzy matching algorithms (e.g., Levenshtein distance)
|
||||
2. **Full-Text Search**: Search within descriptions and documentation
|
||||
3. **Dependency Graph**: Query dependency relationships between skills
|
||||
4. **Version Ranges**: Support semantic version range queries (e.g., `>=1.0.0,<2.0.0`)
|
||||
5. **Sorting Options**: Sort results by name, version, or relevance
|
||||
6. **Regular Expression Support**: Use regex patterns for advanced filtering
|
||||
7. **Marketplace Integration**: Query marketplace catalogs with certification status
|
||||
8. **Performance Caching**: Cache registry data for faster repeated queries
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Find all API-related skills in table format
|
||||
|
||||
```bash
|
||||
$ python3 skills/registry.query/registry_query.py skills --tag api --format table
|
||||
|
||||
================================================================================
|
||||
REGISTRY QUERY: SKILLS
|
||||
================================================================================
|
||||
|
||||
Total entries: 21
|
||||
Matching entries: 5
|
||||
|
||||
+-------------------+---------+--------+---------------------------+-------------------------+
|
||||
| Name | Version | Status | Tags | Commands |
|
||||
+-------------------+---------+--------+---------------------------+-------------------------+
|
||||
| api.define | 0.1.0 | active | api, openapi, asyncapi | /api/define |
|
||||
| api.validate | 0.1.0 | active | api, validation, openapi | /api/validate |
|
||||
| api.compatibility | 0.1.0 | draft | api, compatibility | /api/compatibility |
|
||||
| api.generate | 0.1.0 | active | api, codegen | /api/generate |
|
||||
| api.test | 0.1.0 | draft | api, testing | /api/test |
|
||||
+-------------------+---------+--------+---------------------------+-------------------------+
|
||||
|
||||
================================================================================
|
||||
```
|
||||
|
||||
### Example 2: Find all API-related skills in compact format
|
||||
|
||||
```bash
|
||||
$ python3 skills/registry.query/registry_query.py skills --tag api
|
||||
|
||||
================================================================================
|
||||
REGISTRY QUERY: SKILLS
|
||||
================================================================================
|
||||
|
||||
Total entries: 21
|
||||
Matching entries: 5
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
RESULTS:
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
1. api.define (v0.1.0)
|
||||
Status: active
|
||||
Description: Generates OpenAPI 3.1 or AsyncAPI 2.6 specifications from natural language...
|
||||
Tags: api, openapi, asyncapi, scaffolding
|
||||
Commands: /api/define
|
||||
|
||||
2. api.validate (v0.1.0)
|
||||
Status: active
|
||||
Description: Validates OpenAPI or AsyncAPI specifications against their respective schemas...
|
||||
Tags: api, validation, openapi, asyncapi
|
||||
Commands: /api/validate
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### Example 3: Query hooks registry
|
||||
|
||||
```bash
|
||||
$ python3 skills/registry.query/registry_query.py hooks --format table
|
||||
|
||||
================================================================================
|
||||
REGISTRY QUERY: HOOKS
|
||||
================================================================================
|
||||
|
||||
Total entries: 3
|
||||
Matching entries: 3
|
||||
|
||||
+------------------+---------+--------+------------------+----------------------------------------+---------+
|
||||
| Name | Version | Status | Event | Command | Enabled |
|
||||
+------------------+---------+--------+------------------+----------------------------------------+---------+
|
||||
| pre-commit-lint | 0.1.0 | active | on_commit | python3 hooks/lint.py | True |
|
||||
| auto-test | 0.1.0 | active | on_file_save | pytest tests/ | True |
|
||||
| telemetry-log | 0.1.0 | active | on_workflow_end | python3 hooks/telemetry.py --capture | True |
|
||||
+------------------+---------+--------+------------------+----------------------------------------+---------+
|
||||
|
||||
================================================================================
|
||||
```
|
||||
|
||||
### Example 4: Find agents that can design APIs
|
||||
|
||||
```bash
|
||||
$ python3 skills/registry.query/registry_query.py agents --capability design
|
||||
|
||||
================================================================================
|
||||
REGISTRY QUERY: AGENTS
|
||||
================================================================================
|
||||
|
||||
Total entries: 2
|
||||
Matching entries: 1
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
RESULTS:
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
1. api.designer (v0.1.0)
|
||||
Status: draft
|
||||
Description: Design RESTful APIs following best practices and guidelines...
|
||||
Tags: api, design, openapi
|
||||
Capabilities: 7 capabilities
|
||||
Reasoning: iterative
|
||||
```
|
||||
|
||||
### Example 5: Fuzzy search with limit
|
||||
|
||||
```bash
|
||||
$ python3 skills/registry.query/registry_query.py skills --name vld --fuzzy --limit 3
|
||||
|
||||
# Finds: api.validate, context.validate, etc.
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The skill handles various error conditions:
|
||||
|
||||
- **Invalid registry type**: Returns error with valid options
|
||||
- **Missing registry file**: Returns empty results with warning
|
||||
- **Invalid JSON**: Returns error with details
|
||||
- **Invalid filter combinations**: Logs warnings and proceeds with valid filters
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Registry files are loaded once per query
|
||||
- Filtering is performed in memory (O(n) complexity)
|
||||
- For large registries, use `--limit` to control result size
|
||||
- Consider caching registry data for repeated queries in production
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **None**: This skill has no dependencies on other Betty skills
|
||||
- **Python Standard Library**: Uses `json`, `re`, `pathlib`
|
||||
- **Betty Framework**: Requires `betty.config`, `betty.logging_utils`, `betty.errors`
|
||||
|
||||
## Permissions
|
||||
|
||||
- **`filesystem:read`**: Required to read registry JSON files
|
||||
|
||||
## Contributing
|
||||
|
||||
To extend this skill:
|
||||
|
||||
1. Add new filter types in `filter_entries()`
|
||||
2. Enhance fuzzy matching in `matches_pattern()`
|
||||
3. Add new metadata extractors in `extract_key_metadata()`
|
||||
4. Update tests and documentation
|
||||
|
||||
## See Also
|
||||
|
||||
- **skill.define**: Define new skills
|
||||
- **agent.define**: Define new agents
|
||||
- **plugin.sync**: Sync registry files
|
||||
- **marketplace**: Betty marketplace catalog
|
||||
1
skills/registry.query/__init__.py
Normal file
1
skills/registry.query/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Auto-generated package initializer for skills.
|
||||
735
skills/registry.query/registry_query.py
Executable file
735
skills/registry.query/registry_query.py
Executable file
@@ -0,0 +1,735 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
registry_query.py - Implementation of the registry.query Skill
|
||||
|
||||
Search Betty registries programmatically by filtering skills, agents, and commands.
|
||||
Supports filtering by tags, domain, status, name, version, and capability.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
from typing import Dict, Any, List, Optional, Set
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
from betty.config import (
|
||||
REGISTRY_FILE,
|
||||
AGENTS_REGISTRY_FILE,
|
||||
COMMANDS_REGISTRY_FILE,
|
||||
HOOKS_REGISTRY_FILE,
|
||||
BASE_DIR
|
||||
)
|
||||
from betty.logging_utils import setup_logger
|
||||
from betty.errors import BettyError
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def build_response(
|
||||
ok: bool,
|
||||
errors: Optional[List[str]] = None,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Build standardized response.
|
||||
|
||||
Args:
|
||||
ok: Whether the operation was successful
|
||||
errors: List of error messages
|
||||
details: Additional details to include
|
||||
|
||||
Returns:
|
||||
Standardized response dictionary
|
||||
"""
|
||||
response: Dict[str, Any] = {
|
||||
"ok": ok,
|
||||
"status": "success" if ok else "failed",
|
||||
"errors": errors or [],
|
||||
"timestamp": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
if details is not None:
|
||||
response["details"] = details
|
||||
return response
|
||||
|
||||
|
||||
def load_registry(registry_type: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Load a registry file.
|
||||
|
||||
Args:
|
||||
registry_type: Type of registry ('skills', 'agents', 'commands')
|
||||
|
||||
Returns:
|
||||
Registry data dictionary
|
||||
|
||||
Raises:
|
||||
BettyError: If registry cannot be loaded
|
||||
"""
|
||||
registry_paths = {
|
||||
'skills': REGISTRY_FILE,
|
||||
'agents': AGENTS_REGISTRY_FILE,
|
||||
'commands': COMMANDS_REGISTRY_FILE,
|
||||
'hooks': HOOKS_REGISTRY_FILE
|
||||
}
|
||||
|
||||
if registry_type not in registry_paths:
|
||||
raise BettyError(
|
||||
f"Invalid registry type: {registry_type}",
|
||||
details={
|
||||
"valid_types": list(registry_paths.keys()),
|
||||
"provided": registry_type
|
||||
}
|
||||
)
|
||||
|
||||
registry_path = registry_paths[registry_type]
|
||||
|
||||
if not os.path.exists(registry_path):
|
||||
logger.warning(f"Registry not found: {registry_path}")
|
||||
return {
|
||||
"version": "1.0.0",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
registry_type: []
|
||||
}
|
||||
|
||||
try:
|
||||
with open(registry_path) as f:
|
||||
data = json.load(f)
|
||||
logger.debug(f"Loaded {registry_type} registry: {len(data.get(registry_type, []))} entries")
|
||||
return data
|
||||
except json.JSONDecodeError as e:
|
||||
raise BettyError(f"Invalid JSON in {registry_type} registry: {e}")
|
||||
except Exception as e:
|
||||
raise BettyError(f"Failed to load {registry_type} registry: {e}")
|
||||
|
||||
|
||||
def normalize_filter_value(value: Any) -> str:
|
||||
"""
|
||||
Normalize a filter value for case-insensitive comparison.
|
||||
|
||||
Args:
|
||||
value: Value to normalize
|
||||
|
||||
Returns:
|
||||
Normalized string value
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
return str(value).lower().strip()
|
||||
|
||||
|
||||
def matches_pattern(text: str, pattern: str, fuzzy: bool = False) -> bool:
|
||||
"""
|
||||
Check if text matches a pattern.
|
||||
|
||||
Args:
|
||||
text: Text to match
|
||||
pattern: Pattern to match against
|
||||
fuzzy: Whether to use fuzzy matching
|
||||
|
||||
Returns:
|
||||
True if text matches pattern
|
||||
"""
|
||||
text = normalize_filter_value(text)
|
||||
pattern = normalize_filter_value(pattern)
|
||||
|
||||
if not pattern:
|
||||
return True
|
||||
|
||||
if fuzzy:
|
||||
# Fuzzy match: all characters of pattern appear in order in text
|
||||
pattern_idx = 0
|
||||
for char in text:
|
||||
if pattern_idx < len(pattern) and char == pattern[pattern_idx]:
|
||||
pattern_idx += 1
|
||||
return pattern_idx == len(pattern)
|
||||
else:
|
||||
# Exact substring match
|
||||
return pattern in text
|
||||
|
||||
|
||||
def matches_tags(entry_tags: List[str], filter_tags: List[str]) -> bool:
|
||||
"""
|
||||
Check if entry tags match any of the filter tags.
|
||||
|
||||
Args:
|
||||
entry_tags: Tags from the entry
|
||||
filter_tags: Tags to filter by
|
||||
|
||||
Returns:
|
||||
True if any filter tag is in entry tags
|
||||
"""
|
||||
if not filter_tags:
|
||||
return True
|
||||
|
||||
if not entry_tags:
|
||||
return False
|
||||
|
||||
entry_tags_normalized = [normalize_filter_value(tag) for tag in entry_tags]
|
||||
filter_tags_normalized = [normalize_filter_value(tag) for tag in filter_tags]
|
||||
|
||||
return any(filter_tag in entry_tags_normalized for filter_tag in filter_tags_normalized)
|
||||
|
||||
|
||||
def matches_capabilities(entry_capabilities: List[str], filter_capabilities: List[str], fuzzy: bool = False) -> bool:
|
||||
"""
|
||||
Check if entry capabilities match any of the filter capabilities.
|
||||
|
||||
Args:
|
||||
entry_capabilities: Capabilities from the entry
|
||||
filter_capabilities: Capabilities to filter by
|
||||
fuzzy: Whether to use fuzzy matching
|
||||
|
||||
Returns:
|
||||
True if any capability matches
|
||||
"""
|
||||
if not filter_capabilities:
|
||||
return True
|
||||
|
||||
if not entry_capabilities:
|
||||
return False
|
||||
|
||||
for filter_cap in filter_capabilities:
|
||||
for entry_cap in entry_capabilities:
|
||||
if matches_pattern(entry_cap, filter_cap, fuzzy):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def extract_key_metadata(entry: Dict[str, Any], registry_type: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract key metadata from an entry based on registry type.
|
||||
|
||||
Args:
|
||||
entry: Registry entry
|
||||
registry_type: Type of registry
|
||||
|
||||
Returns:
|
||||
Dictionary with key metadata
|
||||
"""
|
||||
metadata = {
|
||||
"name": entry.get("name"),
|
||||
"version": entry.get("version"),
|
||||
"description": entry.get("description"),
|
||||
"status": entry.get("status"),
|
||||
"tags": entry.get("tags", [])
|
||||
}
|
||||
|
||||
# Add registry-specific metadata
|
||||
if registry_type == "skills":
|
||||
# Handle inputs - can be strings or objects
|
||||
inputs = entry.get("inputs", [])
|
||||
formatted_inputs = []
|
||||
for inp in inputs:
|
||||
if isinstance(inp, str):
|
||||
formatted_inputs.append({"name": inp, "type": "string", "required": False})
|
||||
elif isinstance(inp, dict):
|
||||
formatted_inputs.append({
|
||||
"name": inp.get("name"),
|
||||
"type": inp.get("type"),
|
||||
"required": inp.get("required", False)
|
||||
})
|
||||
|
||||
# Handle outputs - can be strings or objects
|
||||
outputs = entry.get("outputs", [])
|
||||
formatted_outputs = []
|
||||
for out in outputs:
|
||||
if isinstance(out, str):
|
||||
formatted_outputs.append({"name": out, "type": "string"})
|
||||
elif isinstance(out, dict):
|
||||
formatted_outputs.append({
|
||||
"name": out.get("name"),
|
||||
"type": out.get("type")
|
||||
})
|
||||
|
||||
metadata.update({
|
||||
"dependencies": entry.get("dependencies", []),
|
||||
"entrypoints": [
|
||||
{
|
||||
"command": ep.get("command"),
|
||||
"runtime": ep.get("runtime"),
|
||||
"description": ep.get("description")
|
||||
}
|
||||
for ep in entry.get("entrypoints", [])
|
||||
],
|
||||
"inputs": formatted_inputs,
|
||||
"outputs": formatted_outputs
|
||||
})
|
||||
|
||||
elif registry_type == "agents":
|
||||
metadata.update({
|
||||
"capabilities": entry.get("capabilities", []),
|
||||
"skills_available": entry.get("skills_available", []),
|
||||
"reasoning_mode": entry.get("reasoning_mode"),
|
||||
"context_requirements": entry.get("context_requirements", {})
|
||||
})
|
||||
|
||||
elif registry_type == "commands":
|
||||
metadata.update({
|
||||
"execution": entry.get("execution", {}),
|
||||
"parameters": entry.get("parameters", [])
|
||||
})
|
||||
|
||||
elif registry_type == "hooks":
|
||||
metadata.update({
|
||||
"event": entry.get("event"),
|
||||
"command": entry.get("command"),
|
||||
"enabled": entry.get("enabled", True)
|
||||
})
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def filter_entries(
|
||||
entries: List[Dict[str, Any]],
|
||||
registry_type: str,
|
||||
name: Optional[str] = None,
|
||||
version: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
capability: Optional[str] = None,
|
||||
domain: Optional[str] = None,
|
||||
fuzzy: bool = False
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Filter entries based on criteria.
|
||||
|
||||
Args:
|
||||
entries: List of registry entries
|
||||
registry_type: Type of registry
|
||||
name: Filter by name (substring match)
|
||||
version: Filter by version (exact match)
|
||||
status: Filter by status (exact match)
|
||||
tags: Filter by tags (any match)
|
||||
capability: Filter by capability (for agents, substring match)
|
||||
domain: Filter by domain/tag (alias for tags filter)
|
||||
fuzzy: Use fuzzy matching for name and capability
|
||||
|
||||
Returns:
|
||||
List of matching entries with key metadata
|
||||
"""
|
||||
results = []
|
||||
|
||||
# Convert domain to tags if provided
|
||||
if domain:
|
||||
tags = tags or []
|
||||
if domain not in tags:
|
||||
tags.append(domain)
|
||||
|
||||
logger.debug(f"Filtering {len(entries)} entries with criteria:")
|
||||
logger.debug(f" name={name}, version={version}, status={status}")
|
||||
logger.debug(f" tags={tags}, capability={capability}, fuzzy={fuzzy}")
|
||||
|
||||
for entry in entries:
|
||||
# Filter by name
|
||||
if name and not matches_pattern(entry.get("name", ""), name, fuzzy):
|
||||
continue
|
||||
|
||||
# Filter by version (exact match)
|
||||
if version and normalize_filter_value(entry.get("version")) != normalize_filter_value(version):
|
||||
continue
|
||||
|
||||
# Filter by status (exact match)
|
||||
if status and normalize_filter_value(entry.get("status")) != normalize_filter_value(status):
|
||||
continue
|
||||
|
||||
# Filter by tags
|
||||
if tags and not matches_tags(entry.get("tags", []), tags):
|
||||
continue
|
||||
|
||||
# Filter by capability (agents only)
|
||||
if capability:
|
||||
if registry_type == "agents":
|
||||
capabilities = entry.get("capabilities", [])
|
||||
if not matches_capabilities(capabilities, [capability], fuzzy):
|
||||
continue
|
||||
else:
|
||||
# For non-agents, skip capability filter
|
||||
logger.debug(f"Capability filter only applies to agents, skipping for {registry_type}")
|
||||
|
||||
# Entry matches all criteria
|
||||
metadata = extract_key_metadata(entry, registry_type)
|
||||
results.append(metadata)
|
||||
|
||||
logger.info(f"Found {len(results)} matching entries")
|
||||
return results
|
||||
|
||||
|
||||
def query_registry(
|
||||
registry: str,
|
||||
name: Optional[str] = None,
|
||||
version: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
tag: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
capability: Optional[str] = None,
|
||||
domain: Optional[str] = None,
|
||||
fuzzy: bool = False,
|
||||
limit: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Query a Betty registry with filters.
|
||||
|
||||
Args:
|
||||
registry: Registry to query ('skills', 'agents', 'commands')
|
||||
name: Filter by name
|
||||
version: Filter by version
|
||||
status: Filter by status
|
||||
tag: Single tag filter (convenience parameter)
|
||||
tags: List of tags to filter by
|
||||
capability: Filter by capability (agents only)
|
||||
domain: Domain/tag filter (alias for tags)
|
||||
fuzzy: Use fuzzy matching
|
||||
limit: Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
Query result with matching entries
|
||||
"""
|
||||
logger.info(f"Querying {registry} registry")
|
||||
|
||||
# Normalize registry type
|
||||
registry = registry.lower()
|
||||
if registry not in ['skills', 'agents', 'commands', 'hooks']:
|
||||
raise BettyError(
|
||||
f"Invalid registry: {registry}",
|
||||
details={
|
||||
"valid_registries": ["skills", "agents", "commands", "hooks"],
|
||||
"provided": registry
|
||||
}
|
||||
)
|
||||
|
||||
# Load registry
|
||||
registry_data = load_registry(registry)
|
||||
entries = registry_data.get(registry, [])
|
||||
|
||||
# Merge tag and tags parameters
|
||||
if tag:
|
||||
tags = tags or []
|
||||
if tag not in tags:
|
||||
tags.append(tag)
|
||||
|
||||
# Filter entries
|
||||
results = filter_entries(
|
||||
entries,
|
||||
registry,
|
||||
name=name,
|
||||
version=version,
|
||||
status=status,
|
||||
tags=tags,
|
||||
capability=capability,
|
||||
domain=domain,
|
||||
fuzzy=fuzzy
|
||||
)
|
||||
|
||||
# Apply limit
|
||||
if limit and limit > 0:
|
||||
results = results[:limit]
|
||||
|
||||
# Build response
|
||||
return build_response(
|
||||
ok=True,
|
||||
details={
|
||||
"registry": registry,
|
||||
"query": {
|
||||
"name": name,
|
||||
"version": version,
|
||||
"status": status,
|
||||
"tags": tags,
|
||||
"capability": capability,
|
||||
"domain": domain,
|
||||
"fuzzy": fuzzy,
|
||||
"limit": limit
|
||||
},
|
||||
"total_entries": len(entries),
|
||||
"matching_entries": len(results),
|
||||
"results": results
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def format_table(results: List[Dict[str, Any]], registry_type: str) -> str:
|
||||
"""
|
||||
Format results as an aligned table.
|
||||
|
||||
Args:
|
||||
results: List of matching entries
|
||||
registry_type: Type of registry
|
||||
|
||||
Returns:
|
||||
Formatted table string
|
||||
"""
|
||||
if not results:
|
||||
return "No matching entries found."
|
||||
|
||||
# Define columns based on registry type
|
||||
if registry_type == "skills":
|
||||
columns = ["Name", "Version", "Status", "Tags", "Commands"]
|
||||
elif registry_type == "agents":
|
||||
columns = ["Name", "Version", "Status", "Tags", "Reasoning", "Skills"]
|
||||
elif registry_type == "commands":
|
||||
columns = ["Name", "Version", "Status", "Tags", "Execution Type"]
|
||||
elif registry_type == "hooks":
|
||||
columns = ["Name", "Version", "Status", "Event", "Command", "Enabled"]
|
||||
else:
|
||||
columns = ["Name", "Version", "Status", "Description"]
|
||||
|
||||
# Extract data for each column
|
||||
rows = []
|
||||
for entry in results:
|
||||
if registry_type == "skills":
|
||||
commands = [ep.get('command', '') for ep in entry.get('entrypoints', [])]
|
||||
row = [
|
||||
entry.get('name', ''),
|
||||
entry.get('version', ''),
|
||||
entry.get('status', ''),
|
||||
', '.join(entry.get('tags', [])[:3]), # Limit to 3 tags
|
||||
', '.join(commands[:2]) # Limit to 2 commands
|
||||
]
|
||||
elif registry_type == "agents":
|
||||
row = [
|
||||
entry.get('name', ''),
|
||||
entry.get('version', ''),
|
||||
entry.get('status', ''),
|
||||
', '.join(entry.get('tags', [])[:3]),
|
||||
entry.get('reasoning_mode', ''),
|
||||
str(len(entry.get('skills_available', [])))
|
||||
]
|
||||
elif registry_type == "commands":
|
||||
exec_type = entry.get('execution', {}).get('type', '')
|
||||
row = [
|
||||
entry.get('name', ''),
|
||||
entry.get('version', ''),
|
||||
entry.get('status', ''),
|
||||
', '.join(entry.get('tags', [])[:3]),
|
||||
exec_type
|
||||
]
|
||||
elif registry_type == "hooks":
|
||||
row = [
|
||||
entry.get('name', ''),
|
||||
entry.get('version', ''),
|
||||
entry.get('status', ''),
|
||||
entry.get('event', ''),
|
||||
entry.get('command', '')[:40], # Truncate long commands
|
||||
str(entry.get('enabled', True))
|
||||
]
|
||||
else:
|
||||
row = [
|
||||
entry.get('name', ''),
|
||||
entry.get('version', ''),
|
||||
entry.get('status', ''),
|
||||
entry.get('description', '')[:50]
|
||||
]
|
||||
rows.append(row)
|
||||
|
||||
# Calculate column widths
|
||||
col_widths = [len(col) for col in columns]
|
||||
for row in rows:
|
||||
for i, cell in enumerate(row):
|
||||
col_widths[i] = max(col_widths[i], len(str(cell)))
|
||||
|
||||
# Build table
|
||||
lines = []
|
||||
separator = "+" + "+".join("-" * (w + 2) for w in col_widths) + "+"
|
||||
|
||||
# Header
|
||||
lines.append(separator)
|
||||
header = "|" + "|".join(f" {col:<{col_widths[i]}} " for i, col in enumerate(columns)) + "|"
|
||||
lines.append(header)
|
||||
lines.append(separator)
|
||||
|
||||
# Rows
|
||||
for row in rows:
|
||||
row_str = "|" + "|".join(f" {str(cell):<{col_widths[i]}} " for i, cell in enumerate(row)) + "|"
|
||||
lines.append(row_str)
|
||||
|
||||
lines.append(separator)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Query Betty registries programmatically",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# List all skills (compact format)
|
||||
registry_query.py skills
|
||||
|
||||
# Find skills with 'api' tag in table format
|
||||
registry_query.py skills --tag api --format table
|
||||
|
||||
# Find agents with 'design' capability
|
||||
registry_query.py agents --capability design
|
||||
|
||||
# Find active skills with name containing 'validate'
|
||||
registry_query.py skills --name validate --status active
|
||||
|
||||
# Query hooks registry
|
||||
registry_query.py hooks --status active --format table
|
||||
|
||||
# Fuzzy search for commands
|
||||
registry_query.py commands --name test --fuzzy
|
||||
|
||||
# Limit results with JSON output
|
||||
registry_query.py skills --tag api --limit 5 --format json
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"registry",
|
||||
choices=["skills", "agents", "commands", "hooks"],
|
||||
help="Registry to query"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
help="Filter by name (substring match)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
help="Filter by version (exact match)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--status",
|
||||
help="Filter by status (e.g., active, draft, deprecated)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tag",
|
||||
help="Filter by single tag"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tags",
|
||||
nargs="+",
|
||||
help="Filter by multiple tags (any match)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--capability",
|
||||
help="Filter by capability (agents only)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--domain",
|
||||
help="Filter by domain (alias for tag)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fuzzy",
|
||||
action="store_true",
|
||||
help="Use fuzzy matching for name and capability"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
type=int,
|
||||
help="Maximum number of results to return"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["json", "table", "compact"],
|
||||
default="compact",
|
||||
help="Output format (default: compact)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
result = query_registry(
|
||||
registry=args.registry,
|
||||
name=args.name,
|
||||
version=args.version,
|
||||
status=args.status,
|
||||
tag=args.tag,
|
||||
tags=args.tags,
|
||||
capability=args.capability,
|
||||
domain=args.domain,
|
||||
fuzzy=args.fuzzy,
|
||||
limit=args.limit
|
||||
)
|
||||
|
||||
details = result["details"]
|
||||
|
||||
if args.format == "json":
|
||||
# Output full JSON
|
||||
print(json.dumps(result, indent=2))
|
||||
elif args.format == "table":
|
||||
# Table format
|
||||
print(f"\n{'='*80}")
|
||||
print(f"REGISTRY QUERY: {details['registry'].upper()}")
|
||||
print(f"{'='*80}")
|
||||
print(f"\nTotal entries: {details['total_entries']}")
|
||||
print(f"Matching entries: {details['matching_entries']}\n")
|
||||
|
||||
if details['results']:
|
||||
print(format_table(details['results'], details['registry']))
|
||||
else:
|
||||
print("No matching entries found.")
|
||||
|
||||
print(f"\n{'='*80}\n")
|
||||
else:
|
||||
# Compact format (original pretty print)
|
||||
print(f"\n{'='*80}")
|
||||
print(f"REGISTRY QUERY: {details['registry'].upper()}")
|
||||
print(f"{'='*80}")
|
||||
print(f"\nTotal entries: {details['total_entries']}")
|
||||
print(f"Matching entries: {details['matching_entries']}")
|
||||
|
||||
if details['results']:
|
||||
print(f"\n{'-'*80}")
|
||||
print("RESULTS:")
|
||||
print(f"{'-'*80}\n")
|
||||
|
||||
for i, entry in enumerate(details['results'], 1):
|
||||
print(f"{i}. {entry['name']} (v{entry['version']})")
|
||||
print(f" Status: {entry['status']}")
|
||||
print(f" Description: {entry['description'][:80]}...")
|
||||
if entry.get('tags'):
|
||||
print(f" Tags: {', '.join(entry['tags'])}")
|
||||
|
||||
# Registry-specific details
|
||||
if details['registry'] == 'skills':
|
||||
if entry.get('entrypoints'):
|
||||
commands = [ep['command'] for ep in entry['entrypoints']]
|
||||
print(f" Commands: {', '.join(commands)}")
|
||||
elif details['registry'] == 'agents':
|
||||
if entry.get('capabilities'):
|
||||
print(f" Capabilities: {len(entry['capabilities'])} capabilities")
|
||||
print(f" Reasoning: {entry.get('reasoning_mode', 'unknown')}")
|
||||
elif details['registry'] == 'hooks':
|
||||
print(f" Event: {entry.get('event', 'unknown')}")
|
||||
print(f" Command: {entry.get('command', 'unknown')}")
|
||||
print(f" Enabled: {entry.get('enabled', True)}")
|
||||
|
||||
print()
|
||||
|
||||
print(f"{'-'*80}")
|
||||
else:
|
||||
print("\nNo matching entries found.")
|
||||
|
||||
print(f"\n{'='*80}\n")
|
||||
|
||||
sys.exit(0 if result['ok'] else 1)
|
||||
|
||||
except BettyError as e:
|
||||
logger.error(f"Query failed: {e}")
|
||||
result = build_response(
|
||||
ok=False,
|
||||
errors=[str(e)]
|
||||
)
|
||||
print(json.dumps(result, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}", exc_info=True)
|
||||
result = build_response(
|
||||
ok=False,
|
||||
errors=[f"Unexpected error: {str(e)}"]
|
||||
)
|
||||
print(json.dumps(result, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
131
skills/registry.query/skill.yaml
Normal file
131
skills/registry.query/skill.yaml
Normal file
@@ -0,0 +1,131 @@
|
||||
name: registry.query
|
||||
version: 0.1.0
|
||||
description: >
|
||||
Search Betty registries programmatically by filtering skills, agents, commands, and hooks.
|
||||
Supports filtering by tags, domain, status, name, version, and capability with optional
|
||||
fuzzy matching for dynamic discovery and CLI autocompletion. Includes table formatting
|
||||
for easy viewing in CLI.
|
||||
|
||||
inputs:
|
||||
- name: registry
|
||||
type: string
|
||||
required: true
|
||||
description: Registry to query (skills, agents, commands, or hooks)
|
||||
- name: name
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by name (substring match, supports fuzzy matching)
|
||||
- name: version
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by version (exact match)
|
||||
- name: status
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by status (e.g., active, draft, deprecated, archived)
|
||||
- name: tag
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by single tag
|
||||
- name: tags
|
||||
type: array
|
||||
required: false
|
||||
description: Filter by multiple tags (any match)
|
||||
- name: capability
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by capability (agents only, substring match)
|
||||
- name: domain
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by domain (alias for tag filtering)
|
||||
- name: fuzzy
|
||||
type: boolean
|
||||
required: false
|
||||
description: Enable fuzzy matching for name and capability filters
|
||||
- name: limit
|
||||
type: integer
|
||||
required: false
|
||||
description: Maximum number of results to return
|
||||
- name: format
|
||||
type: string
|
||||
required: false
|
||||
description: Output format (json, table, compact)
|
||||
|
||||
outputs:
|
||||
- name: results
|
||||
type: array
|
||||
description: List of matching registry entries with key metadata
|
||||
- name: query_metadata
|
||||
type: object
|
||||
description: Query statistics including total entries and matching count
|
||||
- name: registry_info
|
||||
type: object
|
||||
description: Information about the queried registry
|
||||
|
||||
dependencies: []
|
||||
|
||||
status: active
|
||||
|
||||
entrypoints:
|
||||
- command: /registry/query
|
||||
handler: registry_query.py
|
||||
runtime: python
|
||||
description: >
|
||||
Query Betty registries with flexible filtering options. Returns matching entries
|
||||
with key metadata for programmatic use or CLI exploration.
|
||||
parameters:
|
||||
- name: registry
|
||||
type: string
|
||||
required: true
|
||||
description: Registry to query (skills, agents, commands, or hooks)
|
||||
- name: name
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by name (substring match)
|
||||
- name: version
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by version (exact match)
|
||||
- name: status
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by status
|
||||
- name: tag
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by single tag
|
||||
- name: tags
|
||||
type: array
|
||||
required: false
|
||||
description: Filter by multiple tags
|
||||
- name: capability
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by capability (agents only)
|
||||
- name: domain
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by domain
|
||||
- name: fuzzy
|
||||
type: boolean
|
||||
required: false
|
||||
description: Enable fuzzy matching
|
||||
- name: limit
|
||||
type: integer
|
||||
required: false
|
||||
description: Maximum results to return
|
||||
- name: format
|
||||
type: string
|
||||
required: false
|
||||
description: Output format (json, table, compact)
|
||||
permissions:
|
||||
- filesystem:read
|
||||
|
||||
tags:
|
||||
- registry
|
||||
- search
|
||||
- query
|
||||
- discovery
|
||||
- metadata
|
||||
- cli
|
||||
Reference in New Issue
Block a user