claude-configs/skills/major-domo/cli.py
Cal Corum 43d32e9b9d Update major-domo skill CLI refactor and plugin/config updates
- Refactor major-domo skill: api_client.py, cli.py, and CLI modules (admin, common, injuries, results, schedule, transactions) with significant simplification (-275 lines net)
- Update CLI_REFERENCE.md and SKILL.md docs for major-domo
- Update create-scheduled-task SKILL.md
- Update plugins blocklist.json and known_marketplaces.json
- Add patterns/ directory to repo
- Update CLAUDE.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 02:00:41 -05:00

590 lines
19 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Major Domo CLI - SBA League Management
A command-line interface for the Major Domo API, primarily for use with Claude Code.
Modular architecture: each command group is a separate cli_*.py file.
Usage:
majordomo status
majordomo player get "Mike Trout"
majordomo player search "trout"
majordomo player move "Mike Trout" CAR
majordomo player move --batch "Name1:Team1,Name2:Team2"
majordomo team list
majordomo team roster CAR
majordomo standings --division ALE
majordomo transactions --team CLS
majordomo injuries --active
majordomo stats batting --sort woba --min-pa 100
Environment:
API_TOKEN: Bearer token for API authentication (required for write operations only).
"""
import os
import sys
from typing import Annotated, Optional
import typer
from rich.panel import Panel
# Ensure skill directory is on path for imports
sys.path.insert(0, os.path.dirname(__file__))
from api_client import MajorDomoAPI
from cli_common import (
console,
state,
output_json,
output_table,
handle_error,
get_season,
safe_nested,
)
# ============================================================================
# App Setup
# ============================================================================
app = typer.Typer(
name="majordomo",
help="Major Domo SBA League Management CLI",
no_args_is_help=True,
)
player_app = typer.Typer(help="Player operations")
team_app = typer.Typer(help="Team operations")
app.add_typer(player_app, name="player")
app.add_typer(team_app, name="team")
# Import and mount sub-app modules
from cli_transactions import transactions_app
from cli_injuries import injuries_app
from cli_stats import stats_app
from cli_results import results_app
from cli_schedule import schedule_app
from cli_admin import admin_app
app.add_typer(transactions_app, name="transactions")
app.add_typer(injuries_app, name="injuries")
app.add_typer(stats_app, name="stats")
app.add_typer(results_app, name="results")
app.add_typer(schedule_app, name="schedule")
app.add_typer(admin_app, name="admin")
# ============================================================================
# Main Callback (Global Options)
# ============================================================================
@app.callback()
def main(
env: Annotated[
str, typer.Option("--env", help="Environment: prod or dev")
] = "prod",
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
verbose: Annotated[
bool, typer.Option("--verbose", "-v", help="Verbose output")
] = False,
):
"""Major Domo SBA League Management CLI"""
try:
state.api = MajorDomoAPI(environment=env, verbose=verbose)
state.json_output = json_output
except Exception as e:
handle_error(e)
# ============================================================================
# Status & Health Commands
# ============================================================================
@app.command()
def status():
"""Show current season/week status"""
try:
current = state.api.get_current()
if state.json_output:
output_json(current)
return
panel = Panel(
f"[bold]Season:[/bold] {current['season']}\n"
f"[bold]Week:[/bold] {current['week']}\n"
f"[bold]Frozen:[/bold] {'Yes' if current.get('freeze') else 'No'}\n"
f"[bold]Trade Deadline:[/bold] Week {current.get('trade_deadline', 'N/A')}\n"
f"[bold]Playoffs Begin:[/bold] Week {current.get('playoffs_begin', 'N/A')}",
title="SBA League Status",
border_style="green",
)
console.print(panel)
except Exception as e:
handle_error(e)
@app.command()
def health():
"""Check API health status"""
try:
healthy = state.api.health_check()
if healthy:
console.print(f"[green]API is healthy[/green] ({state.api.base_url})")
else:
console.print("[red]API is not responding[/red]")
raise typer.Exit(1)
except Exception as e:
handle_error(e)
# ============================================================================
# Player Commands
# ============================================================================
@player_app.command("get")
def player_get(
name: Annotated[str, typer.Argument(help="Player name")],
season: Annotated[
Optional[int], typer.Option("--season", "-s", help="Season number")
] = None,
):
"""Get player information by name"""
try:
season = get_season(season)
player = state.api.get_player(name=name, season=season)
if not player:
console.print(
f"[yellow]Player '{name}' not found in season {season}[/yellow]"
)
console.print('Try: majordomo player search "partial name"')
raise typer.Exit(1)
if state.json_output:
output_json(player)
return
# Extract nested team info
team_abbrev = safe_nested(player, "team", "abbrev")
# Collect positions
positions = [
player.get(f"pos_{i}") for i in range(1, 9) if player.get(f"pos_{i}")
]
panel = Panel(
f"[bold]ID:[/bold] {player['id']}\n"
f"[bold]Name:[/bold] {player['name']}\n"
f"[bold]Team:[/bold] {team_abbrev}\n"
f"[bold]Position(s):[/bold] {', '.join(positions) if positions else 'N/A'}\n"
f"[bold]WARA:[/bold] {player.get('wara', 0):.2f}\n"
f"[bold]Strat Code:[/bold] {player.get('strat_code', 'N/A')}\n"
f"[bold]IL Return:[/bold] {player.get('il_return') or 'Healthy'}",
title=f"Player: {player['name']}",
border_style="blue",
)
console.print(panel)
except typer.Exit:
raise
except Exception as e:
handle_error(e, f"Player '{name}' may not exist.")
@player_app.command("search")
def player_search(
query: Annotated[str, typer.Argument(help="Search query (partial name)")],
season: Annotated[
Optional[int], typer.Option("--season", "-s", help="Season number")
] = None,
limit: Annotated[int, typer.Option("--limit", "-n", help="Max results")] = 10,
):
"""Search players by name (fuzzy match)"""
try:
season = get_season(season)
players = state.api.search_players(query=query, season=season, limit=limit)
if state.json_output:
output_json(players)
return
if not players:
console.print(
f"[yellow]No players found matching '{query}' in season {season}[/yellow]"
)
return
rows = []
for p in players:
team_abbrev = safe_nested(p, "team", "abbrev")
rows.append(
[
p["id"],
p["name"],
team_abbrev,
p.get("pos_1", ""),
f"{p.get('wara', 0):.2f}",
]
)
output_table(
f"Search Results: '{query}' (Season {season})",
["ID", "Name", "Team", "Pos", "WARA"],
rows,
)
except Exception as e:
handle_error(e)
@player_app.command("move")
def player_move(
name: Annotated[Optional[str], typer.Argument(help="Player name")] = None,
team: Annotated[
Optional[str], typer.Argument(help="Target team abbreviation")
] = None,
batch: Annotated[
Optional[str],
typer.Option("--batch", "-b", help="Batch moves: 'Name1:Team1,Name2:Team2'"),
] = None,
season: Annotated[
Optional[int], typer.Option("--season", "-s", help="Season number")
] = None,
dry_run: Annotated[
bool, typer.Option("--dry-run", help="Show what would be done")
] = False,
):
"""Move player(s) to a new team"""
try:
season = get_season(season)
# Parse moves
if batch:
moves = []
for move_str in batch.split(","):
move_str = move_str.strip()
if not move_str:
continue
parts = move_str.split(":")
if len(parts) != 2 or not parts[0].strip() or not parts[1].strip():
console.print(f"[red]Invalid batch format:[/red] '{move_str}'")
console.print("Expected format: 'Player Name:TEAM'")
raise typer.Exit(1)
moves.append((parts[0].strip(), parts[1].strip().upper()))
elif name and team:
moves = [(name, team.upper())]
else:
console.print(
"[red]Error:[/red] Provide player name and team, or use --batch"
)
console.print("\nUsage:")
console.print(' majordomo player move "Mike Trout" CAR')
console.print(
' majordomo player move --batch "Mike Trout:CAR,Aaron Judge:NYM"'
)
raise typer.Exit(1)
results = []
for player_name, team_abbrev in moves:
# Find player
player = state.api.get_player(name=player_name, season=season)
if not player:
# Try search
search_results = state.api.search_players(
query=player_name, season=season, limit=1
)
if search_results:
player = search_results[0]
else:
results.append(
(
player_name,
team_abbrev,
"[red]ERROR[/red]",
"Player not found",
)
)
continue
# Get current team
current_abbrev = safe_nested(player, "team", "abbrev")
# Find target team
try:
target_team = state.api.get_team(abbrev=team_abbrev, season=season)
except Exception:
results.append(
(
player_name,
team_abbrev,
"[red]ERROR[/red]",
f"Team '{team_abbrev}' not found",
)
)
continue
if dry_run:
results.append(
(
player["name"],
team_abbrev,
"[blue]DRY-RUN[/blue]",
f"Would move from {current_abbrev}",
)
)
continue
# Perform update
state.api.update_player(player["id"], team_id=target_team["id"])
results.append(
(
player["name"],
team_abbrev,
"[green]SUCCESS[/green]",
f"Moved from {current_abbrev}",
)
)
if state.json_output:
json_results = [
{
"player": r[0],
"team": r[1],
"status": r[2].replace("[", "").replace("]", "").split("/")[0],
"message": r[3],
}
for r in results
]
output_json(json_results)
return
title = "Player Moves"
if dry_run:
title += " (DRY RUN)"
output_table(title, ["Player", "Target", "Status", "Message"], results)
except typer.Exit:
raise
except Exception as e:
handle_error(e)
# ============================================================================
# Team Commands
# ============================================================================
@team_app.command("list")
def team_list(
season: Annotated[
Optional[int], typer.Option("--season", "-s", help="Season number")
] = None,
active: Annotated[
bool, typer.Option("--active", "-a", help="Exclude IL/MiL teams")
] = False,
):
"""List all teams"""
try:
season = get_season(season)
teams = state.api.list_teams(season=season, active_only=active)
if state.json_output:
output_json(teams)
return
if not teams:
console.print(f"[yellow]No teams found in season {season}[/yellow]")
return
rows = []
for t in teams:
manager_name = safe_nested(t, "manager1", "name", default="")
div_abbrev = safe_nested(t, "division", "division_abbrev", default="")
rows.append(
[
t["abbrev"],
t.get("lname", t.get("sname", "")),
div_abbrev,
manager_name,
]
)
output_table(
f"Teams - Season {season}" + (" (Active Only)" if active else ""),
["Abbrev", "Name", "Division", "Manager"],
rows,
)
except Exception as e:
handle_error(e)
@team_app.command("get")
def team_get(
abbrev: Annotated[str, typer.Argument(help="Team abbreviation")],
season: Annotated[
Optional[int], typer.Option("--season", "-s", help="Season number")
] = None,
):
"""Get team information"""
try:
season = get_season(season)
team = state.api.get_team(abbrev=abbrev.upper(), season=season)
if state.json_output:
output_json(team)
return
manager_name = safe_nested(team, "manager1", "name")
div_name = safe_nested(team, "division", "division_name")
salary_cap = team.get("salary_cap")
cap_str = f"{salary_cap:.1f}" if salary_cap is not None else "N/A"
panel = Panel(
f"[bold]ID:[/bold] {team['id']}\n"
f"[bold]Abbreviation:[/bold] {team['abbrev']}\n"
f"[bold]Short Name:[/bold] {team.get('sname', 'N/A')}\n"
f"[bold]Full Name:[/bold] {team.get('lname', 'N/A')}\n"
f"[bold]Division:[/bold] {div_name}\n"
f"[bold]Manager:[/bold] {manager_name}\n"
f"[bold]Salary Cap:[/bold] {cap_str}\n"
f"[bold]Stadium:[/bold] {team.get('stadium', 'N/A')}\n"
f"[bold]Season:[/bold] {team.get('season', 'N/A')}",
title=f"Team: {team.get('lname', abbrev)}",
border_style="green",
)
console.print(panel)
except Exception as e:
handle_error(e, f"Team '{abbrev}' may not exist.")
@team_app.command("roster")
def team_roster(
abbrev: Annotated[str, typer.Argument(help="Team abbreviation")],
which: Annotated[
str, typer.Option("--which", "-w", help="'current' or 'next'")
] = "current",
season: Annotated[
Optional[int], typer.Option("--season", "-s", help="Season number")
] = None,
):
"""Show team roster breakdown"""
try:
season = get_season(season)
team = state.api.get_team(abbrev=abbrev.upper(), season=season)
roster = state.api.get_team_roster(team_id=team["id"], which=which)
if state.json_output:
output_json(roster)
return
console.print(
f"\n[bold cyan]{team.get('lname', abbrev)} Roster ({which.title()})[/bold cyan]\n"
)
# Roster sections: (API key, display label, columns, row builder)
sections = [
(
"active",
"Active",
["Name", "Pos", "WARA"],
lambda p: [p["name"], p.get("pos_1", ""), f"{p.get('wara', 0):.2f}"],
),
(
"shortil",
"Injured List",
["Name", "Pos", "Return"],
lambda p: [p["name"], p.get("pos_1", ""), p.get("il_return", "")],
),
(
"longil",
"Minor League",
["Name", "Pos", "Return"],
lambda p: [p["name"], p.get("pos_1", ""), p.get("il_return", "")],
),
]
total = 0
for i, (key, label, columns, row_fn) in enumerate(sections):
players = roster.get(key, {}).get("players", [])
if players:
if i > 0:
console.print()
output_table(
f"{label} ({len(players)})", columns, [row_fn(p) for p in players]
)
total += len(players)
console.print(f"\n[dim]Total: {total} players[/dim]")
except Exception as e:
handle_error(e, f"Team '{abbrev}' may not exist.")
# ============================================================================
# Standings Command
# ============================================================================
@app.command()
def standings(
season: Annotated[
Optional[int], typer.Option("--season", "-s", help="Season number")
] = None,
division: Annotated[
Optional[str],
typer.Option(
"--division", "-d", help="Division (ALE, ALW, ALC, NLE, NLW, NLC)"
),
] = None,
league: Annotated[
Optional[str], typer.Option("--league", "-l", help="League (AL or NL)")
] = None,
):
"""Show league standings"""
try:
season = get_season(season)
standings_data = state.api.get_standings(
season=season,
division_abbrev=division.upper() if division else None,
league_abbrev=league.upper() if league else None,
)
if state.json_output:
output_json(standings_data)
return
if not standings_data:
console.print(f"[yellow]No standings found for season {season}[/yellow]")
return
rows = []
for s in standings_data:
team_abbrev = safe_nested(s, "team", "abbrev")
team_name = safe_nested(s, "team", "lname")
wins = s.get("wins", 0)
losses = s.get("losses", 0)
total = wins + losses
pct = f".{int(wins/total*1000):03d}" if total > 0 else ".000"
rd = s.get("run_diff", 0)
rd_str = f"+{rd}" if rd > 0 else str(rd)
rows.append([team_abbrev, team_name, wins, losses, pct, rd_str])
title = f"Standings - Season {season}"
if division:
title += f" ({division.upper()})"
elif league:
title += f" ({league.upper()})"
output_table(title, ["Team", "Name", "W", "L", "PCT", "RD"], rows)
except Exception as e:
handle_error(e)
# ============================================================================
# Entry Point
# ============================================================================
if __name__ == "__main__":
app()