- Add all 21 commands (clawd, ralph, prometheus*, dexto*) - Add all hooks (intelligent-router, clawd-*, prometheus-wrapper, unified-integration-v2) - Add skills (ralph, prometheus master) - Add MCP servers (registry.json, manager.sh) - Add plugins directory with marketplaces - Add health-check.sh and aliases.sh scripts - Complete repository synchronization with local ~/.claude/ Total changes: 100+ new files added All integrations now fully backed up in repository 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
652 lines
22 KiB
Python
652 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Ralph Superpowers Integration
|
|
|
|
Complete integration of oh-my-opencode and superpowers features.
|
|
This module dynamically loads, configures, and makes available all skills,
|
|
agents, hooks, and MCPs from both projects for use in Claude Code CLI.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Any, Callable
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
import importlib.util
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger('ralph.superpowers')
|
|
|
|
|
|
class IntegrationType(Enum):
|
|
"""Types of integrations"""
|
|
SKILL = "skill"
|
|
HOOK = "hook"
|
|
AGENT = "agent"
|
|
MCP = "mcp"
|
|
COMMAND = "command"
|
|
TOOL = "tool"
|
|
|
|
|
|
@dataclass
|
|
class IntegrationModule:
|
|
"""Represents an integrated module"""
|
|
name: str
|
|
type: IntegrationType
|
|
source: str # oh-my-opencode, superpowers, contains-studio
|
|
path: str
|
|
enabled: bool = True
|
|
config: Dict = field(default_factory=dict)
|
|
dependencies: List[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class SuperpowersConfig:
|
|
"""Configuration for superpowers integration"""
|
|
# Skills from superpowers
|
|
brainstorming_enabled: bool = True
|
|
writing_plans_enabled: bool = True
|
|
executing_plans_enabled: bool = True
|
|
subagent_driven_dev_enabled: bool = True
|
|
test_driven_dev_enabled: bool = True
|
|
systematic_debugging_enabled: bool = True
|
|
verification_enabled: bool = True
|
|
code_review_enabled: bool = True
|
|
git_worktrees_enabled: bool = True
|
|
|
|
# Hooks from oh-my-opencode
|
|
atlas_enabled: bool = True
|
|
claude_code_hooks_enabled: bool = True
|
|
ralph_loop_enabled: bool = True
|
|
todo_enforcer_enabled: bool = True
|
|
|
|
# Agents from oh-my-opencode
|
|
sisyphus_enabled: bool = True
|
|
oracle_enabled: bool = True
|
|
librarian_enabled: bool = True
|
|
explore_enabled: bool = True
|
|
prometheus_enabled: bool = True
|
|
|
|
# MCPs from oh-my-opencode
|
|
websearch_enabled: bool = True
|
|
context7_enabled: bool = True
|
|
grep_app_enabled: bool = True
|
|
|
|
# Contains-studio agents
|
|
contains_studio_enabled: bool = True
|
|
auto_delegate_enabled: bool = True
|
|
proactive_agents_enabled: bool = True
|
|
|
|
|
|
class SuperpowersIntegration:
|
|
"""
|
|
Main integration class for all superpowers features
|
|
|
|
Manages:
|
|
- Dynamic loading of skills from superpowers
|
|
- Dynamic loading of hooks from oh-my-opencode
|
|
- Dynamic loading of agents from both projects
|
|
- MCP configuration and management
|
|
- Command registration
|
|
- Tool integration
|
|
"""
|
|
|
|
def __init__(self, config: Optional[SuperpowersConfig] = None):
|
|
"""Initialize the integration"""
|
|
self.config = config or SuperpowersConfig()
|
|
self.modules: Dict[str, IntegrationModule] = {}
|
|
self.skill_hooks: Dict[str, List[Callable]] = {}
|
|
self.hook_registry: Dict[str, Callable] = {}
|
|
|
|
# Paths
|
|
self.ralph_dir = Path.home() / '.claude' / 'skills' / 'ralph'
|
|
self.superpowers_dir = self.ralph_dir / 'superpowers'
|
|
self.oh_my_opencode_dir = self.ralph_dir / 'oh-my-opencode'
|
|
self.contains_studio_dir = Path.home() / '.claude' / 'agents'
|
|
|
|
logger.info("Superpowers Integration initialized")
|
|
|
|
def install_all(self) -> Dict[str, Any]:
|
|
"""
|
|
Install and integrate all features
|
|
|
|
Returns:
|
|
Installation summary
|
|
"""
|
|
summary = {
|
|
'skills': [],
|
|
'hooks': [],
|
|
'agents': [],
|
|
'mcps': [],
|
|
'commands': [],
|
|
'errors': []
|
|
}
|
|
|
|
try:
|
|
# 1. Install superpowers skills
|
|
if self.config.brainstorming_enabled:
|
|
self._install_superpowers_skills(summary)
|
|
|
|
# 2. Install oh-my-opencode hooks
|
|
if self.config.atlas_enabled:
|
|
self._install_oh_my_opencode_hooks(summary)
|
|
|
|
# 3. Install agents
|
|
if self.config.sisyphus_enabled or self.config.contains_studio_enabled:
|
|
self._install_agents(summary)
|
|
|
|
# 4. Install MCPs
|
|
if self.config.websearch_enabled:
|
|
self._install_mcps(summary)
|
|
|
|
# 5. Register commands
|
|
self._register_commands(summary)
|
|
|
|
# 6. Create configuration files
|
|
self._create_config_files()
|
|
|
|
logger.info(f"Installation complete: {summary}")
|
|
return summary
|
|
|
|
except Exception as e:
|
|
logger.error(f"Installation failed: {e}")
|
|
summary['errors'].append(str(e))
|
|
return summary
|
|
|
|
def _install_superpowers_skills(self, summary: Dict):
|
|
"""Install skills from superpowers"""
|
|
logger.info("Installing superpowers skills...")
|
|
|
|
skills_dir = self.superpowers_dir / 'skills'
|
|
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Define skills to install
|
|
skills = [
|
|
'brainstorming',
|
|
'writing-plans',
|
|
'executing-plans',
|
|
'subagent-driven-development',
|
|
'test-driven-development',
|
|
'systematic-debugging',
|
|
'verification-before-completion',
|
|
'requesting-code-review',
|
|
'receiving-code-review',
|
|
'using-git-worktrees',
|
|
'finishing-a-development-branch',
|
|
'dispatching-parallel-agents',
|
|
'using-superpowers',
|
|
'writing-skills'
|
|
]
|
|
|
|
for skill in skills:
|
|
try:
|
|
# Copy skill from superpowers source
|
|
source = Path('/tmp/superpowers/skills') / skill
|
|
if source.exists():
|
|
dest = skills_dir / skill
|
|
if dest.exists():
|
|
shutil.rmtree(dest)
|
|
shutil.copytree(source, dest)
|
|
|
|
module = IntegrationModule(
|
|
name=skill,
|
|
type=IntegrationType.SKILL,
|
|
source='superpowers',
|
|
path=str(dest),
|
|
enabled=True
|
|
)
|
|
|
|
self.modules[skill] = module
|
|
summary['skills'].append(skill)
|
|
|
|
logger.info(f" ✓ Installed skill: {skill}")
|
|
|
|
except Exception as e:
|
|
logger.warning(f" ✗ Failed to install skill {skill}: {e}")
|
|
summary['errors'].append(f"skill:{skill} - {e}")
|
|
|
|
def _install_oh_my_opencode_hooks(self, summary: Dict):
|
|
"""Install hooks from oh-my-opencode"""
|
|
logger.info("Installing oh-my-opencode hooks...")
|
|
|
|
hooks_dir = self.oh_my_opencode_dir / 'hooks'
|
|
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Key hooks to install
|
|
hooks = [
|
|
'atlas', # Main orchestrator
|
|
'claude-code-hooks', # Claude Code compatibility
|
|
'ralph-loop', # Autonomous iteration
|
|
'todo-continuation-enforcer', # Task completion
|
|
'thinking-block-validator', # Validate thinking
|
|
'session-recovery', # Recovery from errors
|
|
'edit-error-recovery', # Recovery from edit errors
|
|
'start-work', # Work initialization
|
|
]
|
|
|
|
for hook in hooks:
|
|
try:
|
|
source = Path('/tmp/oh-my-opencode/src/hooks') / hook
|
|
if source.exists():
|
|
dest = hooks_dir / hook
|
|
if dest.exists():
|
|
shutil.rmtree(dest)
|
|
shutil.copytree(source, dest)
|
|
|
|
module = IntegrationModule(
|
|
name=hook,
|
|
type=IntegrationType.HOOK,
|
|
source='oh-my-opencode',
|
|
path=str(dest),
|
|
enabled=True
|
|
)
|
|
|
|
self.modules[hook] = module
|
|
summary['hooks'].append(hook)
|
|
|
|
logger.info(f" ✓ Installed hook: {hook}")
|
|
|
|
except Exception as e:
|
|
logger.warning(f" ✗ Failed to install hook {hook}: {e}")
|
|
summary['errors'].append(f"hook:{hook} - {e}")
|
|
|
|
def _install_agents(self, summary: Dict):
|
|
"""Install agents from both projects"""
|
|
logger.info("Installing agents...")
|
|
|
|
# oh-my-opencode agents
|
|
if self.config.sisyphus_enabled:
|
|
omo_agents_dir = self.oh_my_opencode_dir / 'agents'
|
|
omo_agents_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
agents = [
|
|
'sisyphus',
|
|
'oracle',
|
|
'librarian',
|
|
'explore',
|
|
'prometheus'
|
|
]
|
|
|
|
for agent in agents:
|
|
try:
|
|
source = Path('/tmp/oh-my-opencode/src/agents') / f"{agent}.ts"
|
|
if source.exists():
|
|
dest = omo_agents_dir / f"{agent}.md"
|
|
# Convert TypeScript agent to Markdown format for Claude Code
|
|
self._convert_agent_to_md(source, dest)
|
|
|
|
module = IntegrationModule(
|
|
name=agent,
|
|
type=IntegrationType.AGENT,
|
|
source='oh-my-opencode',
|
|
path=str(dest),
|
|
enabled=True
|
|
)
|
|
|
|
self.modules[agent] = module
|
|
summary['agents'].append(agent)
|
|
|
|
logger.info(f" ✓ Installed agent: {agent}")
|
|
|
|
except Exception as e:
|
|
logger.warning(f" ✗ Failed to install agent {agent}: {e}")
|
|
summary['errors'].append(f"agent:{agent} - {e}")
|
|
|
|
# contains-studio agents (already handled by contains-studio integration)
|
|
if self.config.contains_studio_enabled:
|
|
summary['agents'].append('contains-studio-agents (30+ agents)')
|
|
logger.info(f" ✓ Contains-studio agents already integrated")
|
|
|
|
def _install_mcps(self, summary: Dict):
|
|
"""Install MCPs from oh-my-opencode"""
|
|
logger.info("Installing MCPs...")
|
|
|
|
mcps_dir = self.oh_my_opencode_dir / 'mcps'
|
|
mcps_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
mcps = [
|
|
'websearch',
|
|
'context7',
|
|
'grep_app'
|
|
]
|
|
|
|
for mcp in mcps:
|
|
try:
|
|
source = Path('/tmp/oh-my-opencode/src/mcp') / f"{mcp}.ts"
|
|
if source.exists():
|
|
dest = mcps_dir / f"{mcp}.json"
|
|
|
|
# Create MCP config
|
|
mcp_config = self._create_mcp_config(mcp, source)
|
|
with open(dest, 'w') as f:
|
|
json.dump(mcp_config, f, indent=2)
|
|
|
|
module = IntegrationModule(
|
|
name=mcp,
|
|
type=IntegrationType.MCP,
|
|
source='oh-my-opencode',
|
|
path=str(dest),
|
|
enabled=True
|
|
)
|
|
|
|
self.modules[mcp] = module
|
|
summary['mcps'].append(mcp)
|
|
|
|
logger.info(f" ✓ Installed MCP: {mcp}")
|
|
|
|
except Exception as e:
|
|
logger.warning(f" ✗ Failed to install MCP {mcp}: {e}")
|
|
summary['errors'].append(f"mcp:{mcp} - {e}")
|
|
|
|
def _register_commands(self, summary: Dict):
|
|
"""Register commands from both projects"""
|
|
logger.info("Registering commands...")
|
|
|
|
commands_dir = Path.home() / '.claude' / 'commands'
|
|
|
|
# Ralph sub-commands
|
|
ralph_commands = [
|
|
('brainstorm', 'Interactive design refinement'),
|
|
('write-plan', 'Create implementation plan'),
|
|
('execute-plan', 'Execute plan in batches'),
|
|
('debug', 'Systematic debugging'),
|
|
('review', 'Code review'),
|
|
('status', 'Show Ralph status'),
|
|
('list-agents', 'List all available agents'),
|
|
('list-skills', 'List all available skills')
|
|
]
|
|
|
|
for cmd_name, description in ralph_commands:
|
|
try:
|
|
cmd_file = commands_dir / f'ralph-{cmd_name}.md'
|
|
|
|
content = f"""---
|
|
description: "{description}"
|
|
disable-model-invocation: true
|
|
---
|
|
|
|
Invoke ralph:{cmd_name} via the ralph skill
|
|
"""
|
|
|
|
with open(cmd_file, 'w') as f:
|
|
f.write(content)
|
|
|
|
summary['commands'].append(f'ralph-{cmd_name}')
|
|
logger.info(f" ✓ Registered command: /ralph-{cmd_name}")
|
|
|
|
except Exception as e:
|
|
logger.warning(f" ✗ Failed to register command {cmd_name}: {e}")
|
|
summary['errors'].append(f"command:{cmd_name} - {e}")
|
|
|
|
def _create_config_files(self):
|
|
"""Create configuration files"""
|
|
logger.info("Creating configuration files...")
|
|
|
|
config_dir = Path.home() / '.claude' / 'config'
|
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Main Ralph config
|
|
config_file = config_dir / 'ralph.json'
|
|
|
|
config = {
|
|
'superpowers': {
|
|
'enabled': True,
|
|
'skills': {
|
|
'brainstorming': self.config.brainstorming_enabled,
|
|
'writing-plans': self.config.writing_plans_enabled,
|
|
'executing-plans': self.config.executing_plans_enabled,
|
|
'subagent-driven-development': self.config.subagent_driven_dev_enabled,
|
|
'test-driven-development': self.config.test_driven_dev_enabled,
|
|
'systematic-debugging': self.config.systematic_debugging_enabled,
|
|
'verification-before-completion': self.config.verification_enabled,
|
|
'requesting-code-review': self.config.code_review_enabled,
|
|
'receiving-code-review': self.config.code_review_enabled,
|
|
'using-git-worktrees': self.config.git_worktrees_enabled,
|
|
'finishing-a-development-branch': True,
|
|
'dispatching-parallel-agents': True
|
|
},
|
|
'hooks': {
|
|
'atlas': self.config.atlas_enabled,
|
|
'claude-code-hooks': self.config.claude_code_hooks_enabled,
|
|
'ralph-loop': self.config.ralph_loop_enabled,
|
|
'todo-continuation-enforcer': self.config.todo_enforcer_enabled
|
|
},
|
|
'agents': {
|
|
'sisyphus': self.config.sisyphus_enabled,
|
|
'oracle': self.config.oracle_enabled,
|
|
'librarian': self.config.librarian_enabled,
|
|
'explore': self.config.explore_enabled,
|
|
'prometheus': self.config.prometheus_enabled,
|
|
'contains-studio': self.config.contains_studio_enabled
|
|
},
|
|
'mcps': {
|
|
'websearch': self.config.websearch_enabled,
|
|
'context7': self.config.context7_enabled,
|
|
'grep_app': self.config.grep_app_enabled
|
|
},
|
|
'auto_delegate': self.config.auto_delegate_enabled,
|
|
'proactive_agents': self.config.proactive_agents_enabled
|
|
},
|
|
'multi_agent': {
|
|
'enabled': os.getenv('RALPH_MULTI_AGENT', '').lower() == 'true',
|
|
'max_workers': int(os.getenv('RALPH_MAX_WORKERS', 12)),
|
|
'min_workers': int(os.getenv('RALPH_MIN_WORKERS', 2))
|
|
}
|
|
}
|
|
|
|
with open(config_file, 'w') as f:
|
|
json.dump(config, f, indent=2)
|
|
|
|
logger.info(f" ✓ Created config: {config_file}")
|
|
|
|
def _convert_agent_to_md(self, source_ts: Path, dest_md: Path):
|
|
"""Convert TypeScript agent to Markdown format for Claude Code"""
|
|
# Read TypeScript source
|
|
with open(source_ts, 'r') as f:
|
|
content = f.read()
|
|
|
|
# Extract key information
|
|
# This is a simplified conversion - real implementation would parse TS properly
|
|
|
|
md_content = f"""---
|
|
name: {source_ts.stem}
|
|
description: "Agent from oh-my-opencode: {source_ts.stem}"
|
|
color: blue
|
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
|
---
|
|
|
|
# {source_ts.stem.title()} Agent
|
|
|
|
This agent was imported from oh-my-opencode.
|
|
|
|
## Purpose
|
|
|
|
{self._extract_purpose(content)}
|
|
|
|
## Capabilities
|
|
|
|
- Multi-model orchestration
|
|
- Specialized tool usage
|
|
- Background task management
|
|
- Advanced code analysis
|
|
|
|
## Integration
|
|
|
|
This agent integrates with Ralph's multi-agent system for coordinated task execution.
|
|
"""
|
|
|
|
with open(dest_md, 'w') as f:
|
|
f.write(md_content)
|
|
|
|
def _extract_purpose(self, ts_content: str) -> str:
|
|
"""Extract purpose description from TypeScript content"""
|
|
# Simplified extraction
|
|
if 'orchestrat' in ts_content.lower():
|
|
return "Orchestrates multiple agents and coordinates complex workflows"
|
|
elif 'oracle' in ts_content.lower() or 'consult' in ts_content.lower():
|
|
return "Provides consultation and debugging expertise"
|
|
elif 'librarian' in ts_content.lower() or 'docs' in ts_content.lower():
|
|
return "Searches documentation and codebases"
|
|
elif 'explore' in ts_content.lower() or 'grep' in ts_content.lower():
|
|
return "Fast codebase exploration and search"
|
|
elif 'prometheus' in ts_content.lower() or 'plan' in ts_content.lower():
|
|
return "Strategic planning and task breakdown"
|
|
else:
|
|
return "Specialized AI agent for specific tasks"
|
|
|
|
def _create_mcp_config(self, mcp_name: str, source_file: Path) -> Dict:
|
|
"""Create MCP configuration"""
|
|
# Base MCP config
|
|
configs = {
|
|
'websearch': {
|
|
'name': 'websearch',
|
|
'command': 'npx',
|
|
'args': ['-y', '@modelcontextprotocol/server-exa'],
|
|
'env': {
|
|
'EXA_API_KEY': '${EXA_API_KEY}'
|
|
}
|
|
},
|
|
'context7': {
|
|
'name': 'context7',
|
|
'command': 'npx',
|
|
'args': ['-y', '@context7/mcp-server-docs'],
|
|
'env': {}
|
|
},
|
|
'grep_app': {
|
|
'name': 'grep_app',
|
|
'command': 'npx',
|
|
'args': ['-y', '@modelcontextprotocol/server-github'],
|
|
'env': {
|
|
'GITHUB_TOKEN': '${GITHUB_TOKEN}'
|
|
}
|
|
}
|
|
}
|
|
|
|
return configs.get(mcp_name, {
|
|
'name': mcp_name,
|
|
'command': 'echo',
|
|
'args': ['MCP not configured']
|
|
})
|
|
|
|
def load_skill(self, skill_name: str) -> Optional[Any]:
|
|
"""Dynamically load a skill"""
|
|
skill_key = f"skills.{skill_name}"
|
|
if skill_key not in self.modules:
|
|
logger.warning(f"Skill not found: {skill_name}")
|
|
return None
|
|
|
|
module = self.modules[skill_key]
|
|
|
|
try:
|
|
# Load the skill module
|
|
spec = importlib.util.spec_from_file_location(
|
|
f"ralph.skills.{skill_name}",
|
|
os.path.join(module.path, 'SKILL.md')
|
|
)
|
|
|
|
if spec and spec.loader:
|
|
skill_module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(skill_module)
|
|
return skill_module
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to load skill {skill_name}: {e}")
|
|
|
|
return None
|
|
|
|
def invoke_hook(self, hook_name: str, context: Dict) -> Any:
|
|
"""Invoke a registered hook"""
|
|
if hook_name not in self.hook_registry:
|
|
logger.debug(f"Hook not registered: {hook_name}")
|
|
return None
|
|
|
|
try:
|
|
hook_func = self.hook_registry[hook_name]
|
|
return hook_func(context)
|
|
except Exception as e:
|
|
logger.error(f"Hook {hook_name} failed: {e}")
|
|
return None
|
|
|
|
def register_hook(self, hook_name: str, hook_func: Callable):
|
|
"""Register a hook function"""
|
|
self.hook_registry[hook_name] = hook_func
|
|
logger.info(f"Registered hook: {hook_name}")
|
|
|
|
def get_status(self) -> Dict:
|
|
"""Get integration status"""
|
|
return {
|
|
'modules': {
|
|
name: {
|
|
'type': module.type.value,
|
|
'source': module.source,
|
|
'enabled': module.enabled,
|
|
'path': module.path
|
|
}
|
|
for name, module in self.modules.items()
|
|
},
|
|
'config': {
|
|
'superpowers': {
|
|
'skills_enabled': sum(1 for m in self.modules.values()
|
|
if m.type == IntegrationType.SKILL and m.enabled),
|
|
'hooks_enabled': sum(1 for m in self.modules.values()
|
|
if m.type == IntegrationType.HOOK and m.enabled),
|
|
'agents_enabled': sum(1 for m in self.modules.values()
|
|
if m.type == IntegrationType.AGENT and m.enabled),
|
|
'mcps_enabled': sum(1 for m in self.modules.values()
|
|
if m.type == IntegrationType.MCP and m.enabled)
|
|
}
|
|
},
|
|
'hooks_registered': list(self.hook_registry.keys())
|
|
}
|
|
|
|
|
|
def main():
|
|
"""Main entry point for CLI usage"""
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description='Ralph Superpowers Integration')
|
|
parser.add_argument('--install', action='store_true', help='Install all superpowers')
|
|
parser.add_argument('--status', action='store_true', help='Show integration status')
|
|
parser.add_argument('--config', help='Path to config file')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Load config
|
|
config = SuperpowersConfig()
|
|
if args.config:
|
|
with open(args.config) as f:
|
|
config_data = json.load(f)
|
|
# Apply config...
|
|
|
|
# Create integration
|
|
integration = SuperpowersIntegration(config)
|
|
|
|
if args.install:
|
|
summary = integration.install_all()
|
|
print("\n=== Installation Summary ===")
|
|
print(f"Skills: {len(summary['skills'])}")
|
|
print(f"Hooks: {len(summary['hooks'])}")
|
|
print(f"Agents: {len(summary['agents'])}")
|
|
print(f"MCPs: {len(summary['mcps'])}")
|
|
print(f"Commands: {len(summary['commands'])}")
|
|
if summary['errors']:
|
|
print(f"\nErrors: {len(summary['errors'])}")
|
|
for error in summary['errors']:
|
|
print(f" - {error}")
|
|
|
|
elif args.status:
|
|
status = integration.get_status()
|
|
print(json.dumps(status, indent=2))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|