523 lines
19 KiB
Python
Executable File
523 lines
19 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
generate_marketplace.py - Implementation of the generate.marketplace Skill
|
|
Generates marketplace JSON files from registry entries:
|
|
- marketplace/skills.json
|
|
- marketplace/agents.json
|
|
- marketplace/commands.json
|
|
- marketplace/hooks.json
|
|
|
|
Filters by status: active and certified: true (if present).
|
|
Adds last_updated ISO timestamp to all marketplace files.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
from typing import Dict, Any, List, Optional
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
|
|
from betty.config import BASE_DIR
|
|
from betty.logging_utils import setup_logger
|
|
|
|
logger = setup_logger(__name__)
|
|
|
|
|
|
def load_registry_file(registry_path: str) -> Dict[str, Any]:
|
|
"""
|
|
Load a JSON registry file.
|
|
|
|
Args:
|
|
registry_path: Path to the registry JSON file
|
|
|
|
Returns:
|
|
Parsed registry data
|
|
|
|
Raises:
|
|
FileNotFoundError: If registry file doesn't exist
|
|
json.JSONDecodeError: If JSON is invalid
|
|
"""
|
|
try:
|
|
with open(registry_path) as f:
|
|
return json.load(f)
|
|
except FileNotFoundError:
|
|
logger.error(f"Registry file not found: {registry_path}")
|
|
raise
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"Failed to parse JSON from {registry_path}: {e}")
|
|
raise
|
|
|
|
|
|
def is_certified(item: Dict[str, Any]) -> bool:
|
|
"""
|
|
Check if an item is certified for marketplace inclusion.
|
|
|
|
Args:
|
|
item: Skill or agent entry
|
|
|
|
Returns:
|
|
True if item should be included in marketplace
|
|
"""
|
|
# Filter by status: active
|
|
if item.get("status") != "active":
|
|
return False
|
|
|
|
# If certified field exists, check it
|
|
if "certified" in item:
|
|
return item.get("certified") is True
|
|
|
|
# If no certified field, consider active items as certified
|
|
return True
|
|
|
|
|
|
def convert_skill_to_marketplace(skill: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Convert a registry skill entry to marketplace format.
|
|
|
|
Args:
|
|
skill: Skill entry from registry
|
|
|
|
Returns:
|
|
Skill entry in marketplace format
|
|
"""
|
|
skill_name = skill.get("name")
|
|
marketplace_skill = {
|
|
"name": skill_name,
|
|
"version": skill.get("version"),
|
|
"description": skill.get("description"),
|
|
"status": "certified", # Transform active -> certified for marketplace
|
|
"tags": skill.get("tags", []),
|
|
"manifest_path": f"skills/{skill_name}/skill.yaml",
|
|
"maintainer": skill.get("maintainer", "Betty Core Team"),
|
|
"usage_examples": skill.get("usage_examples", []),
|
|
"documentation_url": f"https://betty-framework.dev/docs/skills/{skill_name}",
|
|
"dependencies": skill.get("dependencies", []),
|
|
"entrypoints": skill.get("entrypoints", []),
|
|
"inputs": skill.get("inputs", []),
|
|
"outputs": skill.get("outputs", [])
|
|
}
|
|
|
|
# Generate usage examples if not present
|
|
if not marketplace_skill["usage_examples"] and marketplace_skill["entrypoints"]:
|
|
examples = []
|
|
for entrypoint in marketplace_skill["entrypoints"]:
|
|
command = entrypoint.get("command", "")
|
|
desc = entrypoint.get("description", skill.get("description", ""))
|
|
if command:
|
|
# Create a simple example from the command
|
|
example = f"Run {skill.get('name')}: {command}"
|
|
examples.append(example.strip())
|
|
marketplace_skill["usage_examples"] = examples[:2] # Limit to 2 examples
|
|
|
|
return marketplace_skill
|
|
|
|
|
|
def convert_agent_to_marketplace(agent: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Convert a registry agent entry to marketplace format.
|
|
|
|
Args:
|
|
agent: Agent entry from registry
|
|
|
|
Returns:
|
|
Agent entry in marketplace format
|
|
"""
|
|
agent_name = agent.get("name")
|
|
marketplace_agent = {
|
|
"name": agent_name,
|
|
"version": agent.get("version"),
|
|
"description": agent.get("description"),
|
|
"status": "certified", # Transform active -> certified for marketplace
|
|
"reasoning_mode": agent.get("reasoning_mode", "oneshot"),
|
|
"skills_available": agent.get("skills_available", []),
|
|
"capabilities": agent.get("capabilities", []),
|
|
"tags": agent.get("tags", []),
|
|
"manifest_path": f"agents/{agent_name}/agent.yaml",
|
|
"maintainer": agent.get("maintainer", "Betty Core Team"),
|
|
"documentation_url": f"https://betty-framework.dev/docs/agents/{agent_name}",
|
|
"dependencies": agent.get("dependencies", [])
|
|
}
|
|
|
|
return marketplace_agent
|
|
|
|
|
|
def convert_command_to_marketplace(command: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Convert a registry command entry to marketplace format.
|
|
|
|
Args:
|
|
command: Command entry from registry
|
|
|
|
Returns:
|
|
Command entry in marketplace format
|
|
"""
|
|
marketplace_command = {
|
|
"name": command.get("name"),
|
|
"version": command.get("version"),
|
|
"description": command.get("description"),
|
|
"status": "certified", # Transform active -> certified for marketplace
|
|
"tags": command.get("tags", []),
|
|
"execution": command.get("execution", {}),
|
|
"parameters": command.get("parameters", []),
|
|
"maintainer": command.get("maintainer", "Betty Core Team")
|
|
}
|
|
|
|
return marketplace_command
|
|
|
|
|
|
def convert_hook_to_marketplace(hook: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Convert a registry hook entry to marketplace format.
|
|
|
|
Args:
|
|
hook: Hook entry from registry
|
|
|
|
Returns:
|
|
Hook entry in marketplace format
|
|
"""
|
|
marketplace_hook = {
|
|
"name": hook.get("name"),
|
|
"version": hook.get("version"),
|
|
"description": hook.get("description"),
|
|
"status": "certified", # Transform active -> certified for marketplace
|
|
"tags": hook.get("tags", []),
|
|
"event": hook.get("event"),
|
|
"command": hook.get("command"),
|
|
"blocking": hook.get("blocking", False),
|
|
"when": hook.get("when", {}),
|
|
"timeout": hook.get("timeout"),
|
|
"on_failure": hook.get("on_failure"),
|
|
"maintainer": hook.get("maintainer", "Betty Core Team")
|
|
}
|
|
|
|
return marketplace_hook
|
|
|
|
|
|
def generate_skills_marketplace(registry_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Generate marketplace skills catalog from registry.
|
|
|
|
Args:
|
|
registry_data: Parsed skills.json from registry
|
|
|
|
Returns:
|
|
Marketplace-formatted skills catalog
|
|
"""
|
|
skills = registry_data.get("skills", [])
|
|
|
|
# Filter and convert active/certified skills
|
|
certified_skills = []
|
|
for skill in skills:
|
|
if is_certified(skill):
|
|
marketplace_skill = convert_skill_to_marketplace(skill)
|
|
certified_skills.append(marketplace_skill)
|
|
logger.info(f"Added certified skill: {skill.get('name')}")
|
|
else:
|
|
logger.debug(f"Skipped non-certified skill: {skill.get('name')} (status: {skill.get('status')})")
|
|
|
|
# Build marketplace catalog
|
|
marketplace = {
|
|
"marketplace_version": "1.0.0",
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
"description": "Betty Framework Certified Skills Marketplace",
|
|
"total_skills": len(skills),
|
|
"certified_count": len(certified_skills),
|
|
"draft_count": len(skills) - len(certified_skills),
|
|
"catalog": certified_skills
|
|
}
|
|
|
|
return marketplace
|
|
|
|
|
|
def generate_agents_marketplace(registry_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Generate marketplace agents catalog from registry.
|
|
|
|
Args:
|
|
registry_data: Parsed agents.json from registry
|
|
|
|
Returns:
|
|
Marketplace-formatted agents catalog
|
|
"""
|
|
agents = registry_data.get("agents", [])
|
|
|
|
# Filter and convert active/certified agents
|
|
certified_agents = []
|
|
for agent in agents:
|
|
if is_certified(agent):
|
|
marketplace_agent = convert_agent_to_marketplace(agent)
|
|
certified_agents.append(marketplace_agent)
|
|
logger.info(f"Added certified agent: {agent.get('name')}")
|
|
else:
|
|
logger.debug(f"Skipped non-certified agent: {agent.get('name')} (status: {agent.get('status')})")
|
|
|
|
# Build marketplace catalog
|
|
marketplace = {
|
|
"marketplace_version": "1.0.0",
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
"description": "Betty Framework Certified Agents Marketplace",
|
|
"total_agents": len(agents),
|
|
"certified_count": len(certified_agents),
|
|
"draft_count": len(agents) - len(certified_agents),
|
|
"catalog": certified_agents
|
|
}
|
|
|
|
return marketplace
|
|
|
|
|
|
def generate_commands_marketplace(registry_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Generate marketplace commands catalog from registry.
|
|
|
|
Args:
|
|
registry_data: Parsed commands.json from registry
|
|
|
|
Returns:
|
|
Marketplace-formatted commands catalog
|
|
"""
|
|
commands = registry_data.get("commands", [])
|
|
|
|
# Filter and convert active/certified commands
|
|
certified_commands = []
|
|
for command in commands:
|
|
if is_certified(command):
|
|
marketplace_command = convert_command_to_marketplace(command)
|
|
certified_commands.append(marketplace_command)
|
|
logger.info(f"Added certified command: {command.get('name')}")
|
|
else:
|
|
logger.debug(f"Skipped non-certified command: {command.get('name')} (status: {command.get('status')})")
|
|
|
|
# Build marketplace catalog
|
|
marketplace = {
|
|
"marketplace_version": "1.0.0",
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
"description": "Betty Framework Certified Commands Marketplace",
|
|
"total_commands": len(commands),
|
|
"certified_count": len(certified_commands),
|
|
"draft_count": len(commands) - len(certified_commands),
|
|
"catalog": certified_commands
|
|
}
|
|
|
|
return marketplace
|
|
|
|
|
|
def generate_hooks_marketplace(registry_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Generate marketplace hooks catalog from registry.
|
|
|
|
Args:
|
|
registry_data: Parsed hooks.json from registry
|
|
|
|
Returns:
|
|
Marketplace-formatted hooks catalog
|
|
"""
|
|
hooks = registry_data.get("hooks", [])
|
|
|
|
# Filter and convert active/certified hooks
|
|
certified_hooks = []
|
|
for hook in hooks:
|
|
if is_certified(hook):
|
|
marketplace_hook = convert_hook_to_marketplace(hook)
|
|
certified_hooks.append(marketplace_hook)
|
|
logger.info(f"Added certified hook: {hook.get('name')}")
|
|
else:
|
|
logger.debug(f"Skipped non-certified hook: {hook.get('name')} (status: {hook.get('status')})")
|
|
|
|
# Build marketplace catalog
|
|
marketplace = {
|
|
"marketplace_version": "1.0.0",
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
"description": "Betty Framework Certified Hooks Marketplace",
|
|
"total_hooks": len(hooks),
|
|
"certified_count": len(certified_hooks),
|
|
"draft_count": len(hooks) - len(certified_hooks),
|
|
"catalog": certified_hooks
|
|
}
|
|
|
|
return marketplace
|
|
|
|
|
|
def write_marketplace_file(data: Dict[str, Any], output_path: str):
|
|
"""
|
|
Write marketplace JSON file with proper formatting.
|
|
|
|
Args:
|
|
data: Marketplace data dictionary
|
|
output_path: Path where to write the file
|
|
"""
|
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
|
|
with open(output_path, 'w') as f:
|
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
|
|
logger.info(f"✅ Written marketplace file to {output_path}")
|
|
|
|
|
|
def generate_claude_code_marketplace() -> Dict[str, Any]:
|
|
"""
|
|
Generate Claude Code marketplace.json file.
|
|
|
|
This file lists Betty Framework as an installable plugin in Claude Code's
|
|
marketplace format, as specified in Claude Code's plugin marketplace documentation.
|
|
|
|
Returns:
|
|
Claude Code marketplace JSON structure
|
|
"""
|
|
# Load plugin.yaml to get current metadata
|
|
plugin_yaml_path = os.path.join(BASE_DIR, "plugin.yaml")
|
|
|
|
try:
|
|
import yaml
|
|
with open(plugin_yaml_path, 'r') as f:
|
|
plugin_data = yaml.safe_load(f)
|
|
except Exception as e:
|
|
logger.warning(f"Could not load plugin.yaml: {e}. Using defaults.")
|
|
plugin_data = {}
|
|
|
|
# Build Claude Code marketplace structure
|
|
marketplace = {
|
|
"name": "betty-marketplace",
|
|
"version": plugin_data.get("version", "1.0.0"),
|
|
"description": "Betty Framework Plugin Marketplace - Enterprise-grade AI-assisted engineering framework",
|
|
"owner": {
|
|
"name": plugin_data.get("author", {}).get("name", "RiskExec"),
|
|
"email": plugin_data.get("author", {}).get("email", "platform@riskexec.com"),
|
|
"url": plugin_data.get("author", {}).get("url", "https://github.com/epieczko/betty")
|
|
},
|
|
"metadata": {
|
|
"homepage": plugin_data.get("metadata", {}).get("homepage", "https://github.com/epieczko/betty"),
|
|
"repository": plugin_data.get("metadata", {}).get("repository", "https://github.com/epieczko/betty"),
|
|
"documentation": plugin_data.get("metadata", {}).get("documentation", "https://github.com/epieczko/betty/tree/main/docs"),
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"generated_by": "generate.marketplace skill"
|
|
},
|
|
"plugins": [
|
|
{
|
|
"name": plugin_data.get("name", "betty-framework"),
|
|
"source": ".",
|
|
"version": plugin_data.get("version", "1.0.0"),
|
|
"description": plugin_data.get("description", "Betty Framework for structured AI-assisted engineering"),
|
|
"author": {
|
|
"name": plugin_data.get("author", {}).get("name", "RiskExec"),
|
|
"email": plugin_data.get("author", {}).get("email", "platform@riskexec.com"),
|
|
"url": plugin_data.get("author", {}).get("url", "https://github.com/epieczko/betty")
|
|
},
|
|
"license": plugin_data.get("license", "MIT"),
|
|
"tags": plugin_data.get("metadata", {}).get("tags", [
|
|
"framework",
|
|
"api-development",
|
|
"workflow",
|
|
"governance",
|
|
"enterprise"
|
|
]),
|
|
"requirements": plugin_data.get("requirements", {
|
|
"python": ">=3.11",
|
|
"packages": ["pyyaml"]
|
|
}),
|
|
"strict": True # Requires plugin.yaml manifest
|
|
}
|
|
]
|
|
}
|
|
|
|
return marketplace
|
|
|
|
|
|
def main():
|
|
"""Main CLI entry point."""
|
|
logger.info("Starting marketplace catalog generation from registries...")
|
|
|
|
# Define registry and output paths
|
|
skills_registry_path = os.path.join(BASE_DIR, "registry", "skills.json")
|
|
agents_registry_path = os.path.join(BASE_DIR, "registry", "agents.json")
|
|
commands_registry_path = os.path.join(BASE_DIR, "registry", "commands.json")
|
|
hooks_registry_path = os.path.join(BASE_DIR, "registry", "hooks.json")
|
|
|
|
marketplace_dir = os.path.join(BASE_DIR, "marketplace")
|
|
skills_output_path = os.path.join(marketplace_dir, "skills.json")
|
|
agents_output_path = os.path.join(marketplace_dir, "agents.json")
|
|
commands_output_path = os.path.join(marketplace_dir, "commands.json")
|
|
hooks_output_path = os.path.join(marketplace_dir, "hooks.json")
|
|
|
|
try:
|
|
# Load registry files
|
|
logger.info("Loading registry files...")
|
|
skills_registry = load_registry_file(skills_registry_path)
|
|
agents_registry = load_registry_file(agents_registry_path)
|
|
commands_registry = load_registry_file(commands_registry_path)
|
|
hooks_registry = load_registry_file(hooks_registry_path)
|
|
|
|
# Generate marketplace catalogs
|
|
logger.info("Generating marketplace catalogs...")
|
|
skills_marketplace = generate_skills_marketplace(skills_registry)
|
|
agents_marketplace = generate_agents_marketplace(agents_registry)
|
|
commands_marketplace = generate_commands_marketplace(commands_registry)
|
|
hooks_marketplace = generate_hooks_marketplace(hooks_registry)
|
|
|
|
# Write Betty marketplace catalogs to files
|
|
logger.info("Writing Betty marketplace files...")
|
|
write_marketplace_file(skills_marketplace, skills_output_path)
|
|
write_marketplace_file(agents_marketplace, agents_output_path)
|
|
write_marketplace_file(commands_marketplace, commands_output_path)
|
|
write_marketplace_file(hooks_marketplace, hooks_output_path)
|
|
|
|
# Generate and write Claude Code marketplace.json
|
|
logger.info("Generating Claude Code marketplace.json...")
|
|
claude_marketplace = generate_claude_code_marketplace()
|
|
claude_marketplace_path = os.path.join(BASE_DIR, ".claude-plugin", "marketplace.json")
|
|
write_marketplace_file(claude_marketplace, claude_marketplace_path)
|
|
|
|
# Report results
|
|
result = {
|
|
"ok": True,
|
|
"status": "success",
|
|
"skills_output": skills_output_path,
|
|
"agents_output": agents_output_path,
|
|
"commands_output": commands_output_path,
|
|
"hooks_output": hooks_output_path,
|
|
"claude_marketplace_output": claude_marketplace_path,
|
|
"skills_certified": skills_marketplace["certified_count"],
|
|
"skills_total": skills_marketplace["total_skills"],
|
|
"agents_certified": agents_marketplace["certified_count"],
|
|
"agents_total": agents_marketplace["total_agents"],
|
|
"commands_certified": commands_marketplace["certified_count"],
|
|
"commands_total": commands_marketplace["total_commands"],
|
|
"hooks_certified": hooks_marketplace["certified_count"],
|
|
"hooks_total": hooks_marketplace["total_hooks"]
|
|
}
|
|
|
|
# Print summary
|
|
logger.info(f"✅ Generated marketplace catalogs:")
|
|
logger.info(f" Skills: {result['skills_certified']}/{result['skills_total']} certified")
|
|
logger.info(f" Agents: {result['agents_certified']}/{result['agents_total']} certified")
|
|
logger.info(f" Commands: {result['commands_certified']}/{result['commands_total']} certified")
|
|
logger.info(f" Hooks: {result['hooks_certified']}/{result['hooks_total']} certified")
|
|
logger.info(f"📄 Outputs:")
|
|
logger.info(f" - {skills_output_path}")
|
|
logger.info(f" - {agents_output_path}")
|
|
logger.info(f" - {commands_output_path}")
|
|
logger.info(f" - {hooks_output_path}")
|
|
logger.info(f" - {claude_marketplace_path} (Claude Code format)")
|
|
|
|
print(json.dumps(result, indent=2))
|
|
sys.exit(0)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate marketplace catalogs: {e}")
|
|
result = {
|
|
"ok": False,
|
|
"status": "failed",
|
|
"error": str(e)
|
|
}
|
|
print(json.dumps(result, indent=2))
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|