claude-configs/skills/paper-dynasty/cli.py
Cal Corum 8a1d15911f Initial commit: Claude Code configuration backup
Version control Claude Code configuration including:
- Global instructions (CLAUDE.md)
- User settings (settings.json)
- Custom agents (architect, designer, engineer, etc.)
- Custom skills (create-skill templates and workflows)

Excludes session data, secrets, cache, and temporary files per .gitignore.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 16:34:21 -06:00

649 lines
21 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Paper Dynasty CLI - Baseball Card Game Management
A command-line interface for the Paper Dynasty API, primarily for use with Claude Code.
Usage:
pd status
pd team list
pd team get SKB
pd team cards SKB
pd pack today
pd pack distribute --num 10
pd gauntlet list --event-id 8 --active
pd gauntlet cleanup Gauntlet-SKB --event-id 8 --yes
Environment:
API_TOKEN: Required. Bearer token for API authentication.
"""
import json
import os
import sys
from typing import Annotated, List, Optional
import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
# Import the existing API client from same directory
sys.path.insert(0, os.path.dirname(__file__))
from api_client import PaperDynastyAPI
# ============================================================================
# App Setup
# ============================================================================
app = typer.Typer(
name="pd",
help="Paper Dynasty Baseball Card Game CLI",
no_args_is_help=True,
)
team_app = typer.Typer(help="Team operations")
pack_app = typer.Typer(help="Pack operations")
gauntlet_app = typer.Typer(help="Gauntlet operations")
player_app = typer.Typer(help="Player operations")
app.add_typer(team_app, name="team")
app.add_typer(pack_app, name="pack")
app.add_typer(gauntlet_app, name="gauntlet")
app.add_typer(player_app, name="player")
console = Console()
class State:
"""Global state for API client and settings"""
api: Optional[PaperDynastyAPI] = None
json_output: bool = False
state = State()
# ============================================================================
# Output Helpers
# ============================================================================
def output_json(data):
"""Output data as formatted JSON"""
console.print_json(json.dumps(data, indent=2, default=str))
def output_table(title: str, columns: List[str], rows: List[List], show_lines: bool = False):
"""Output data as a rich table"""
table = Table(title=title, show_header=True, header_style="bold cyan", show_lines=show_lines)
for col in columns:
table.add_column(col)
for row in rows:
table.add_row(*[str(cell) if cell is not None else "" for cell in row])
console.print(table)
def handle_error(e: Exception, context: str = ""):
"""Graceful error handling with helpful messages"""
error_str = str(e)
if "401" in error_str:
console.print("[red]Error:[/red] Unauthorized. Check your API_TOKEN.")
elif "404" in error_str:
console.print(f"[red]Error:[/red] Not found. {context}")
elif "Connection" in error_str or "ConnectionError" in error_str:
console.print("[red]Error:[/red] Cannot connect to API. Check network and --env setting.")
else:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(1)
# ============================================================================
# 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,
):
"""Paper Dynasty Baseball Card Game CLI"""
try:
state.api = PaperDynastyAPI(environment=env, verbose=verbose)
state.json_output = json_output
except ValueError as e:
console.print(f"[red]Configuration Error:[/red] {e}")
console.print("\nSet API_TOKEN environment variable:")
console.print(" export API_TOKEN='your-token-here'")
raise typer.Exit(1)
except Exception as e:
handle_error(e)
# ============================================================================
# Status & Health Commands
# ============================================================================
@app.command()
def status():
"""Show packs opened today summary"""
try:
result = state.api.get_packs_opened_today()
if state.json_output:
output_json(result)
return
console.print(f"\n[bold cyan]Packs Opened Today ({result['date']})[/bold cyan]\n")
console.print(f"[bold]Total:[/bold] {result['total']} packs\n")
if result['teams']:
rows = []
for t in result['teams']:
rows.append([t['abbrev'], t['name'], t['packs']])
output_table("By Team", ["Abbrev", "Team", "Packs"], rows)
else:
console.print("[dim]No packs opened today[/dim]")
if result.get('note'):
console.print(f"\n[yellow]Note:[/yellow] {result['note']}")
except Exception as e:
handle_error(e)
@app.command()
def health():
"""Check API health status"""
try:
# Try to list teams as a health check
teams = state.api.list_teams()
console.print(f"[green]API is healthy[/green] ({state.api.base_url})")
console.print(f"[dim]Found {len(teams)} teams[/dim]")
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="Filter by season")] = None,
):
"""List all teams"""
try:
teams = state.api.list_teams(season=season)
if state.json_output:
output_json(teams)
return
if not teams:
console.print("[yellow]No teams found[/yellow]")
return
# Filter out gauntlet teams for cleaner display
regular_teams = [t for t in teams if 'Gauntlet' not in t.get('abbrev', '')]
rows = []
for t in regular_teams:
rows.append([
t['abbrev'],
t.get('sname', ''),
t.get('season', ''),
t.get('wallet', 0),
t.get('ranking', 'N/A'),
'AI' if t.get('is_ai') else 'Human'
])
title = "Teams"
if season:
title += f" - Season {season}"
output_table(title, ["Abbrev", "Name", "Season", "Wallet", "Rank", "Type"], rows)
except Exception as e:
handle_error(e)
@team_app.command("get")
def team_get(
abbrev: Annotated[str, typer.Argument(help="Team abbreviation")],
):
"""Get team details"""
try:
team = state.api.get_team(abbrev=abbrev.upper())
if state.json_output:
output_json(team)
return
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]Season:[/bold] {team.get('season', 'N/A')}\n"
f"[bold]Wallet:[/bold] ${team.get('wallet', 0)}\n"
f"[bold]Ranking:[/bold] {team.get('ranking', 'N/A')}\n"
f"[bold]Type:[/bold] {'AI' if team.get('is_ai') else 'Human'}",
title=f"Team: {team.get('lname', abbrev)}",
border_style="green",
)
console.print(panel)
except ValueError as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(1)
except Exception as e:
handle_error(e, f"Team '{abbrev}' may not exist.")
@team_app.command("cards")
def team_cards(
abbrev: Annotated[str, typer.Argument(help="Team abbreviation")],
limit: Annotated[int, typer.Option("--limit", "-n", help="Max cards to show")] = 50,
):
"""List team's cards"""
try:
team = state.api.get_team(abbrev=abbrev.upper())
cards = state.api.list_cards(team_id=team['id'])
if state.json_output:
output_json(cards)
return
if not cards:
console.print(f"[yellow]Team {abbrev} has no cards[/yellow]")
return
rows = []
for c in cards[:limit]:
player = c.get('player', {})
rows.append([
c['id'],
player.get('p_name', 'Unknown'),
player.get('rarity', ''),
c.get('value', 0)
])
output_table(
f"Cards for {team.get('lname', abbrev)} ({len(cards)} total)",
["Card ID", "Player", "Rarity", "Value"],
rows
)
if len(cards) > limit:
console.print(f"\n[dim]Showing {limit} of {len(cards)} cards. Use --limit to see more.[/dim]")
except ValueError as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(1)
except Exception as e:
handle_error(e)
# ============================================================================
# Pack Commands
# ============================================================================
@pack_app.command("list")
def pack_list(
team: Annotated[Optional[str], typer.Option("--team", "-t", help="Filter by team abbrev")] = None,
opened: Annotated[Optional[bool], typer.Option("--opened/--unopened", help="Filter by opened status")] = None,
limit: Annotated[int, typer.Option("--limit", "-n", help="Max packs to show")] = 50,
):
"""List packs"""
try:
team_id = None
team_name = None
if team:
team_obj = state.api.get_team(abbrev=team.upper())
team_id = team_obj['id']
team_name = team_obj.get('sname', team)
packs = state.api.list_packs(team_id=team_id, opened=opened, new_to_old=True, limit=limit)
if state.json_output:
output_json(packs)
return
if not packs:
console.print("[yellow]No packs found[/yellow]")
return
rows = []
for p in packs:
pack_team = p.get('team', {})
pack_type = p.get('pack_type', {})
is_opened = "Yes" if p.get('open_time') else "No"
rows.append([
p['id'],
pack_team.get('abbrev', 'N/A'),
pack_type.get('name', 'Unknown'),
is_opened
])
title = "Packs"
if team_name:
title += f" - {team_name}"
if opened is True:
title += " (Opened)"
elif opened is False:
title += " (Unopened)"
output_table(title, ["Pack ID", "Team", "Type", "Opened"], rows)
except ValueError as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(1)
except Exception as e:
handle_error(e)
@pack_app.command("today")
def pack_today():
"""Show packs opened today analytics"""
# Reuse status command
status()
@pack_app.command("distribute")
def pack_distribute(
num: Annotated[int, typer.Option("--num", "-n", help="Number of packs per team")] = 5,
exclude: Annotated[Optional[List[str]], typer.Option("--exclude", "-x", help="Team abbrevs to exclude")] = None,
pack_type: Annotated[int, typer.Option("--pack-type", help="Pack type ID (1=Standard)")] = 1,
dry_run: Annotated[bool, typer.Option("--dry-run", help="Show what would be done")] = False,
):
"""Distribute packs to all human teams"""
try:
if dry_run:
# Get qualifying teams to show preview
current = state.api.get('current')
season = current['season']
all_teams = state.api.list_teams(season=season)
exclude_upper = [e.upper() for e in (exclude or [])]
qualifying = [
t for t in all_teams
if not t['is_ai']
and 'gauntlet' not in t['abbrev'].lower()
and t['abbrev'].upper() not in exclude_upper
]
console.print(f"\n[bold cyan]Pack Distribution Preview (DRY RUN)[/bold cyan]\n")
console.print(f"[bold]Packs per team:[/bold] {num}")
console.print(f"[bold]Pack type:[/bold] {pack_type}")
console.print(f"[bold]Teams:[/bold] {len(qualifying)}")
console.print(f"[bold]Total packs:[/bold] {num * len(qualifying)}")
if exclude:
console.print(f"[bold]Excluded:[/bold] {', '.join(exclude)}")
console.print("\n[bold]Qualifying teams:[/bold]")
for t in qualifying:
console.print(f" - {t['abbrev']}: {t['sname']}")
return
result = state.api.distribute_packs(
num_packs=num,
exclude_team_abbrev=exclude,
pack_type_id=pack_type
)
if state.json_output:
output_json(result)
return
console.print(f"\n[green]Distribution complete![/green]")
console.print(f"[bold]Total packs:[/bold] {result['total_packs']}")
console.print(f"[bold]Teams:[/bold] {result['teams_count']}")
if exclude:
console.print(f"[bold]Excluded:[/bold] {', '.join(exclude)}")
except Exception as e:
handle_error(e)
# ============================================================================
# Gauntlet Commands
# ============================================================================
@gauntlet_app.command("list")
def gauntlet_list(
event_id: Annotated[Optional[int], typer.Option("--event-id", "-e", help="Filter by event ID")] = None,
active: Annotated[bool, typer.Option("--active", "-a", help="Only active runs")] = False,
):
"""List gauntlet runs"""
try:
runs = state.api.list_gauntlet_runs(event_id=event_id, active_only=active)
if state.json_output:
output_json(runs)
return
if not runs:
console.print("[yellow]No gauntlet runs found[/yellow]")
return
rows = []
for r in runs:
team = r.get('team', {})
is_active = "Active" if r.get('ended', 0) == 0 else "Ended"
rows.append([
r['id'],
team.get('abbrev', 'N/A'),
r.get('wins', 0),
r.get('losses', 0),
r.get('gauntlet_id', 'N/A'),
is_active
])
title = "Gauntlet Runs"
if event_id:
title += f" - Event {event_id}"
if active:
title += " (Active Only)"
output_table(title, ["Run ID", "Team", "W", "L", "Event", "Status"], rows)
except Exception as e:
handle_error(e)
@gauntlet_app.command("teams")
def gauntlet_teams(
event_id: Annotated[Optional[int], typer.Option("--event-id", "-e", help="Filter by event ID")] = None,
active: Annotated[bool, typer.Option("--active", "-a", help="Only teams with active runs")] = False,
):
"""List gauntlet teams"""
try:
teams = state.api.find_gauntlet_teams(event_id=event_id, active_only=active)
if state.json_output:
output_json(teams)
return
if not teams:
console.print("[yellow]No gauntlet teams found[/yellow]")
return
rows = []
for t in teams:
run = t.get('active_run') or t.get('run', {})
wins = run.get('wins', '-') if run else '-'
losses = run.get('losses', '-') if run else '-'
rows.append([
t['id'],
t['abbrev'],
t.get('sname', ''),
wins,
losses
])
title = "Gauntlet Teams"
if active:
title += " (Active)"
output_table(title, ["Team ID", "Abbrev", "Name", "W", "L"], rows)
except Exception as e:
handle_error(e)
@gauntlet_app.command("cleanup")
def gauntlet_cleanup(
team_abbrev: Annotated[str, typer.Argument(help="Team abbreviation (e.g., Gauntlet-SKB)")],
event_id: Annotated[int, typer.Option("--event-id", "-e", help="Event ID (required)")],
yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation")] = False,
):
"""Clean up a gauntlet team (wipe cards, delete packs, end run)"""
try:
# Find the team
team = state.api.get_team(abbrev=team_abbrev)
team_id = team['id']
# Get cards and packs count
cards = state.api.list_cards(team_id=team_id)
packs = state.api.list_packs(team_id=team_id, opened=False)
# Find active run
runs = state.api.list_gauntlet_runs(event_id=event_id, team_id=team_id, active_only=True)
active_run = runs[0] if runs else None
console.print(f"\n[bold cyan]Gauntlet Cleanup: {team_abbrev}[/bold cyan]\n")
console.print(f"[bold]Team ID:[/bold] {team_id}")
console.print(f"[bold]Cards to wipe:[/bold] {len(cards)}")
console.print(f"[bold]Packs to delete:[/bold] {len(packs)}")
console.print(f"[bold]Active run:[/bold] {'Yes (ID: ' + str(active_run['id']) + ')' if active_run else 'No'}")
if not yes:
console.print("\n[yellow]This is a destructive operation![/yellow]")
console.print("Use --yes flag to confirm.")
raise typer.Exit(0)
# Perform cleanup
results = []
# 1. Wipe cards
if cards:
state.api.wipe_team_cards(team_id)
results.append(f"Wiped {len(cards)} cards")
# 2. Delete packs
for pack in packs:
state.api.delete_pack(pack['id'])
if packs:
results.append(f"Deleted {len(packs)} packs")
# 3. End gauntlet run
if active_run:
state.api.end_gauntlet_run(active_run['id'])
results.append(f"Ended run {active_run['id']}")
console.print(f"\n[green]Cleanup complete![/green]")
for r in results:
console.print(f" - {r}")
except ValueError as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(1)
except Exception as e:
handle_error(e)
# ============================================================================
# Player Commands
# ============================================================================
@player_app.command("get")
def player_get(
player_id: Annotated[int, typer.Argument(help="Player ID")],
):
"""Get player by ID"""
try:
player = state.api.get_player(player_id=player_id)
if state.json_output:
output_json(player)
return
# Get positions
positions = []
for i in range(1, 9):
pos = player.get(f'pos{i}')
if pos:
positions.append(pos)
cardset = player.get('cardset', {})
panel = Panel(
f"[bold]ID:[/bold] {player['player_id']}\n"
f"[bold]Name:[/bold] {player.get('p_name', 'Unknown')}\n"
f"[bold]Rarity:[/bold] {player.get('rarity', 'N/A')}\n"
f"[bold]Cost:[/bold] {player.get('cost', 0)}\n"
f"[bold]Positions:[/bold] {', '.join(positions) if positions else 'N/A'}\n"
f"[bold]Cardset:[/bold] {cardset.get('name', 'N/A')} (ID: {cardset.get('id', 'N/A')})\n"
f"[bold]Bats/Throws:[/bold] {player.get('bats', 'N/A')}/{player.get('throws', 'N/A')}",
title=f"Player: {player.get('p_name', 'Unknown')}",
border_style="blue",
)
console.print(panel)
except Exception as e:
handle_error(e, f"Player ID {player_id} may not exist.")
@player_app.command("list")
def player_list(
rarity: Annotated[Optional[str], typer.Option("--rarity", "-r", help="Filter by rarity")] = None,
cardset: Annotated[Optional[int], typer.Option("--cardset", "-c", help="Filter by cardset ID")] = None,
limit: Annotated[int, typer.Option("--limit", "-n", help="Max players to show")] = 50,
):
"""List players"""
try:
players = state.api.list_players(cardset_id=cardset, rarity=rarity)
if state.json_output:
output_json(players)
return
if not players:
console.print("[yellow]No players found[/yellow]")
return
rows = []
for p in players[:limit]:
cs = p.get('cardset', {})
rows.append([
p['player_id'],
p.get('p_name', 'Unknown'),
p.get('rarity', ''),
p.get('cost', 0),
cs.get('name', 'N/A')
])
title = "Players"
if rarity:
title += f" - {rarity}"
if cardset:
title += f" - Cardset {cardset}"
output_table(title, ["ID", "Name", "Rarity", "Cost", "Cardset"], rows)
if len(players) > limit:
console.print(f"\n[dim]Showing {limit} of {len(players)} players. Use --limit to see more.[/dim]")
except Exception as e:
handle_error(e)
# ============================================================================
# Entry Point
# ============================================================================
if __name__ == "__main__":
app()