Migrate Gitea ops to MCP, update Paper Dynasty skill, sync plugins

- CLAUDE.md + commit-push-pr: prefer gitea-mcp over tea CLI
- Paper Dynasty: updated api_client, cli, distribute_packs
- New skill: resume-tailoring
- Plugins: updated marketplaces, blocklist, install counts
- Settings and MCP config updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-02-25 18:59:49 -06:00
parent 7f120f8c5c
commit 8642bb539a
15 changed files with 853 additions and 583 deletions

View File

@ -5,19 +5,5 @@
"type": "stdio",
"args": [],
"env": {}
},
"n8n-mcp": {
"command": "npx",
"type": "stdio",
"args": ["n8n-mcp"],
"env": {
"MCP_MODE": "stdio",
"N8N_API_URL": "http://10.10.0.210:5678",
"N8N_API_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwNGQ1MTNhYi1lOGI1LTQxZjktYmNjNi05MTM1MzgyYzQ0YWEiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzcxNTU3ODMwfQ.PF6AatWZh8hwyUydT8UsJ16ML61XFTixj2IvuP9MRzo",
"N8N_MCP_TELEMETRY_DISABLED": "true",
"DISABLE_CONSOLE_OUTPUT": "true",
"LOG_LEVEL": "error"
}
}
}
} }
}

View File

@ -14,12 +14,12 @@ Automatic loads are NOT enough — Read loads required CLAUDE.md context along t
- Applies to: git commit, git add, git tag, git push, deploy scripts
## Gitea Operations
**ALWAYS use `tea` CLI for Gitea.** Never use `gh api --hostname`.
- Authenticated: `cal@homelab` (https://git.manticorum.com)
- **Always pass `--repo owner/name`** — tea can't auto-detect the repo from git remotes and fails with "path segment is empty"
- Common: `tea repos list`, `tea pulls list --repo cal/repo`, `tea issues list --repo cal/repo`
- Create PR: `tea pulls create --repo cal/repo --head <branch> --base main --title "Title" --description "Desc"`
- Common repos: cal/major-domo-v2, cal/major-domo-database, cal/major-domo-bot, cal/paper-dynasty, cal/paper-dynasty-database
**Prefer the `gitea-mcp` MCP server** for all Gitea operations (PRs, issues, branches, labels, releases, Actions).
- MCP tools use `owner` + `repo` params (e.g. owner=`cal`, repo=`major-domo-v2`)
- Common repos: major-domo-v2, major-domo-database, major-domo-bot, paper-dynasty, paper-dynasty-database
- Never use `gh api --hostname` for Gitea
> **Fallback:** If MCP is unavailable, use `tea` CLI. Always pass `--repo owner/name`.
## Tech Preferences
- Python with uv for package/environment management

View File

@ -1,5 +1,5 @@
---
allowed-tools: Bash(git checkout:*), Bash(git add:*), Bash(git status:*), Bash(git push:*), Bash(git commit:*), Bash(git branch:*), Bash(git remote:*), Bash(git symbolic-ref:*), Bash(git log:*), Bash(gh pr create:*), Bash(tea pulls create:*)
allowed-tools: Bash(git checkout:*), Bash(git add:*), Bash(git status:*), Bash(git push:*), Bash(git commit:*), Bash(git branch:*), Bash(git remote:*), Bash(git symbolic-ref:*), Bash(git log:*), Bash(gh pr create:*), mcp__gitea-mcp__create_pull_request
description: Commit, push, and open a PR
---
@ -25,7 +25,7 @@ Based on the above changes:
6. Push the branch to origin with `-u` flag
7. Create a pull request:
- If remote URL contains `github.com` → use `gh pr create --base <default-branch> --title "Title" --body "..."`
- If remote URL contains `git.manticorum.com` or other Gitea host → use `tea pulls create --head <branch> --base <default-branch> --title "Title" --description "..."`
- If remote URL contains `git.manticorum.com` or other Gitea host → use `mcp__gitea-mcp__create_pull_request` with `owner`, `repo`, `title`, `body`, `head`, `base`
8. Include a summary section and test plan in the PR body
9. Return the PR URL

View File

@ -0,0 +1 @@
{"claude.ai Google Calendar":{"timestamp":1772062089205},"claude.ai Gmail":{"timestamp":1772062089218}}

View File

@ -1,5 +1,5 @@
{
"fetchedAt": "2026-02-20T04:07:24.860Z",
"fetchedAt": "2026-02-25T22:40:22.318Z",
"plugins": [
{
"plugin": "code-review@claude-plugins-official",

View File

@ -1,230 +1,234 @@
{
"version": 1,
"fetchedAt": "2026-02-19T04:03:05.754Z",
"fetchedAt": "2026-02-22T05:53:29.726Z",
"counts": [
{
"plugin": "frontend-design@claude-plugins-official",
"unique_installs": 183759
"unique_installs": 211218
},
{
"plugin": "context7@claude-plugins-official",
"unique_installs": 115503
"unique_installs": 127061
},
{
"plugin": "code-review@claude-plugins-official",
"unique_installs": 91933
},
{
"plugin": "github@claude-plugins-official",
"unique_installs": 83345
},
{
"plugin": "feature-dev@claude-plugins-official",
"unique_installs": 80588
},
{
"plugin": "code-simplifier@claude-plugins-official",
"unique_installs": 73439
"unique_installs": 103962
},
{
"plugin": "superpowers@claude-plugins-official",
"unique_installs": 72147
"unique_installs": 92356
},
{
"plugin": "github@claude-plugins-official",
"unique_installs": 92248
},
{
"plugin": "feature-dev@claude-plugins-official",
"unique_installs": 89510
},
{
"plugin": "code-simplifier@claude-plugins-official",
"unique_installs": 84564
},
{
"plugin": "ralph-loop@claude-plugins-official",
"unique_installs": 67562
"unique_installs": 74996
},
{
"plugin": "typescript-lsp@claude-plugins-official",
"unique_installs": 61765
"unique_installs": 69574
},
{
"plugin": "playwright@claude-plugins-official",
"unique_installs": 60378
"unique_installs": 69512
},
{
"plugin": "commit-commands@claude-plugins-official",
"unique_installs": 52976
},
{
"plugin": "serena@claude-plugins-official",
"unique_installs": 47364
"unique_installs": 58414
},
{
"plugin": "security-guidance@claude-plugins-official",
"unique_installs": 47274
"unique_installs": 53585
},
{
"plugin": "serena@claude-plugins-official",
"unique_installs": 49897
},
{
"plugin": "pr-review-toolkit@claude-plugins-official",
"unique_installs": 35736
},
{
"plugin": "pyright-lsp@claude-plugins-official",
"unique_installs": 32249
},
{
"plugin": "figma@claude-plugins-official",
"unique_installs": 31936
},
{
"plugin": "supabase@claude-plugins-official",
"unique_installs": 31746
"unique_installs": 39827
},
{
"plugin": "claude-md-management@claude-plugins-official",
"unique_installs": 28868
"unique_installs": 39554
},
{
"plugin": "figma@claude-plugins-official",
"unique_installs": 36609
},
{
"plugin": "pyright-lsp@claude-plugins-official",
"unique_installs": 35872
},
{
"plugin": "supabase@claude-plugins-official",
"unique_installs": 34374
},
{
"plugin": "agent-sdk-dev@claude-plugins-official",
"unique_installs": 27597
},
{
"plugin": "ralph-wiggum@claude-plugins-official",
"unique_installs": 27127
"unique_installs": 29685
},
{
"plugin": "atlassian@claude-plugins-official",
"unique_installs": 26859
"unique_installs": 29214
},
{
"plugin": "plugin-dev@claude-plugins-official",
"unique_installs": 21681
},
{
"plugin": "explanatory-output-style@claude-plugins-official",
"unique_installs": 21571
},
{
"plugin": "greptile@claude-plugins-official",
"unique_installs": 20163
"plugin": "ralph-wiggum@claude-plugins-official",
"unique_installs": 27164
},
{
"plugin": "claude-code-setup@claude-plugins-official",
"unique_installs": 19541
"unique_installs": 25066
},
{
"plugin": "plugin-dev@claude-plugins-official",
"unique_installs": 24060
},
{
"plugin": "explanatory-output-style@claude-plugins-official",
"unique_installs": 23970
},
{
"plugin": "greptile@claude-plugins-official",
"unique_installs": 22699
},
{
"plugin": "Notion@claude-plugins-official",
"unique_installs": 18738
"unique_installs": 20513
},
{
"plugin": "hookify@claude-plugins-official",
"unique_installs": 18502
},
{
"plugin": "linear@claude-plugins-official",
"unique_installs": 15749
"unique_installs": 20396
},
{
"plugin": "vercel@claude-plugins-official",
"unique_installs": 15454
"unique_installs": 17352
},
{
"plugin": "linear@claude-plugins-official",
"unique_installs": 17226
},
{
"plugin": "learning-output-style@claude-plugins-official",
"unique_installs": 14537
"unique_installs": 16023
},
{
"plugin": "slack@claude-plugins-official",
"unique_installs": 12797
"unique_installs": 14454
},
{
"plugin": "sentry@claude-plugins-official",
"unique_installs": 12586
"unique_installs": 13925
},
{
"plugin": "gopls-lsp@claude-plugins-official",
"unique_installs": 12195
"unique_installs": 13515
},
{
"plugin": "csharp-lsp@claude-plugins-official",
"unique_installs": 11692
"unique_installs": 13187
},
{
"plugin": "gitlab@claude-plugins-official",
"unique_installs": 10720
},
{
"plugin": "rust-analyzer-lsp@claude-plugins-official",
"unique_installs": 10634
"unique_installs": 11947
},
{
"plugin": "stripe@claude-plugins-official",
"unique_installs": 10475
"unique_installs": 11832
},
{
"plugin": "laravel-boost@claude-plugins-official",
"unique_installs": 9552
},
{
"plugin": "php-lsp@claude-plugins-official",
"unique_installs": 9213
},
{
"plugin": "jdtls-lsp@claude-plugins-official",
"unique_installs": 9152
"plugin": "rust-analyzer-lsp@claude-plugins-official",
"unique_installs": 11762
},
{
"plugin": "playground@claude-plugins-official",
"unique_installs": 8414
"unique_installs": 11389
},
{
"plugin": "php-lsp@claude-plugins-official",
"unique_installs": 10298
},
{
"plugin": "laravel-boost@claude-plugins-official",
"unique_installs": 10204
},
{
"plugin": "jdtls-lsp@claude-plugins-official",
"unique_installs": 10186
},
{
"plugin": "clangd-lsp@claude-plugins-official",
"unique_installs": 8112
},
{
"plugin": "swift-lsp@claude-plugins-official",
"unique_installs": 7732
},
{
"plugin": "firebase@claude-plugins-official",
"unique_installs": 7707
"unique_installs": 8996
},
{
"plugin": "huggingface-skills@claude-plugins-official",
"unique_installs": 7615
"unique_installs": 8946
},
{
"plugin": "firebase@claude-plugins-official",
"unique_installs": 8581
},
{
"plugin": "swift-lsp@claude-plugins-official",
"unique_installs": 8511
},
{
"plugin": "kotlin-lsp@claude-plugins-official",
"unique_installs": 5312
},
{
"plugin": "lua-lsp@claude-plugins-official",
"unique_installs": 4824
"unique_installs": 6192
},
{
"plugin": "coderabbit@claude-plugins-official",
"unique_installs": 4356
"unique_installs": 5806
},
{
"plugin": "asana@claude-plugins-official",
"unique_installs": 3331
"plugin": "lua-lsp@claude-plugins-official",
"unique_installs": 5368
},
{
"plugin": "circleback@claude-plugins-official",
"unique_installs": 3253
"unique_installs": 3806
},
{
"plugin": "asana@claude-plugins-official",
"unique_installs": 3804
},
{
"plugin": "pinecone@claude-plugins-official",
"unique_installs": 2999
"unique_installs": 3449
},
{
"plugin": "firecrawl@claude-plugins-official",
"unique_installs": 3336
},
{
"plugin": "posthog@claude-plugins-official",
"unique_installs": 2827
},
{
"plugin": "claude-opus-4-5-migration@claude-plugins-official",
"unique_installs": 2714
},
{
"plugin": "posthog@claude-plugins-official",
"unique_installs": 2258
},
{
"plugin": "firecrawl@claude-plugins-official",
"unique_installs": 1486
},
{
"plugin": "sonatype-guide@claude-plugins-official",
"unique_installs": 931
"unique_installs": 2111
},
{
"plugin": "skill-creator@claude-plugins-official",
"unique_installs": 327
},
{
"plugin": "figma-mcp@claude-plugins-official",
"unique_installs": 99
"unique_installs": 101
},
{
"plugin": "artifact@claude-plugins-official",
@ -235,7 +239,11 @@
"unique_installs": 31
},
{
"plugin": "pm@claude-plugins-official",
"plugin": "document-skills@claude-plugins-official",
"unique_installs": 2
},
{
"plugin": "agent-browser@claude-plugins-official",
"unique_installs": 2
},
{
@ -243,187 +251,11 @@
"unique_installs": 2
},
{
"plugin": "codex-skills@claude-plugins-official",
"unique_installs": 1
"plugin": "pm@claude-plugins-official",
"unique_installs": 2
},
{
"plugin": "dev-workflow@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "datadog@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "omnisharp-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "perlnavigator-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "bun-typescript@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "typescript-native-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "docs-search-tool@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "feature-ears@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "n8n@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "lorikeet-qa@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "vectorhub-memory@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "project-collaboration-system@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "openspec@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "claude-memory@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "hardworking@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "terraform-ls@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "design-principles@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "creative-music-output-style@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "freshservice@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "n8n-skills@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "context@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "dj-content-creator@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "csharp-roslyn-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "memory-agent@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "my-time-plugin@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "miro@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "prototyper@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "lean-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "agent-browser@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "gdscript-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "dev-sandbox@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "ccpm@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "gitlab-mr-review@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "review-submission@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "document-skills@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "forge-security@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "ocpm@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "pyrefly-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "hosts-db@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "test-automation-generator@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "frontend-lab@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "dune@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "dokploy@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "monday@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "microsoft-learn@claude-plugins-official",
"plugin": "hello-world@claude-plugins-official",
"unique_installs": 1
},
{
@ -431,11 +263,43 @@
"unique_installs": 1
},
{
"plugin": "backend-specialist@claude-plugins-official",
"plugin": "ocpm@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "autonomous-loop@claude-plugins-official",
"plugin": "omnisharp-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "dev-sandbox@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "feature-ears@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "context@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "creative-music-output-style@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "latex2cn@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "pdf2latex@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "n8n-skills@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "user-journey-analysis@claude-plugins-official",
"unique_installs": 1
},
{
@ -443,43 +307,235 @@
"unique_installs": 1
},
{
"plugin": "it-triage-system@claude-plugins-official",
"plugin": "memory-agent@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "airtable@claude-plugins-official",
"plugin": "lean-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "claude-rules-generator@claude-plugins-official",
"plugin": "forge-security@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "beast-plan@claude-plugins-official",
"plugin": "typescript-native-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "aws-diagram@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "freshservice@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "design-principles@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "project-collaboration-system@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "n8n@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "rs-commands@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "docs-search-tool@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "dune@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "perlnavigator-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "claude-memory@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "agent-teams@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "vertical-builder@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "gemini-consult@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "codeceptjs-e2e-tests@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "csharp-roslyn-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "dev-workflow@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "dokploy@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "autonomous-loop@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "spec-writer@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "my-time-plugin@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "hardworking@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "monday@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "lorikeet-qa@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "miro@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "gdscript-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "datadog@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "airtable@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "beast-plan@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "vectorhub-memory@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "pyrefly-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "jira@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "openspec@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "review-submission@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "microsoft-learn@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "it-triage-system@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "prototype@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "backend-specialist@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "dj-content-creator@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "gitlab-mr-review@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "frontend-lab@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "prototyper@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "universal-dev@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "ccpm@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "terraform-ls@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "silince-gutnebrg-builder@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "codex-skills@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "ai-pm-copilot@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "bun-typescript@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "claude-rules-generator@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "test-automation-generator@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "plan-guardian@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "hosts-db@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "dart-lsp@claude-plugins-official",
"unique_installs": 1
},
{
"plugin": "plan-guardian@claude-plugins-official",
"plugin": "amber-electric@claude-plugins-official",
"unique_installs": 1
}
]

View File

@ -15,10 +15,10 @@
"playground@claude-plugins-official": [
{
"scope": "user",
"installPath": "/home/cal/.claude/plugins/cache/claude-plugins-official/playground/8deab8460a9d",
"version": "8deab8460a9d",
"installPath": "/home/cal/.claude/plugins/cache/claude-plugins-official/playground/55b58ec6e564",
"version": "55b58ec6e564",
"installedAt": "2026-02-18T19:51:28.422Z",
"lastUpdated": "2026-02-19T00:58:15.373Z",
"lastUpdated": "2026-02-25T18:35:41.447Z",
"gitCommitSha": "261ce4fba4f2c314c490302158909a32e5889c88"
}
],
@ -31,6 +31,16 @@
"lastUpdated": "2026-02-19T04:07:27.304Z",
"gitCommitSha": "8deab8460a9d4df5a01315ef722a5ca6b061c074"
}
],
"frontend-design@claude-plugins-official": [
{
"scope": "user",
"installPath": "/home/cal/.claude/plugins/cache/claude-plugins-official/frontend-design/55b58ec6e564",
"version": "55b58ec6e564",
"installedAt": "2026-02-22T05:53:45.091Z",
"lastUpdated": "2026-02-25T18:35:41.440Z",
"gitCommitSha": "aa296ec81e8ccb49c9784f167c2c0aa625a86cec"
}
]
}
}

View File

@ -5,7 +5,7 @@
"url": "https://github.com/anthropics/claude-plugins-official.git"
},
"installLocation": "/home/cal/.claude/plugins/marketplaces/claude-plugins-official",
"lastUpdated": "2026-02-19T11:00:01.550Z"
"lastUpdated": "2026-02-22T05:54:15.140Z"
},
"claude-code-plugins": {
"source": {
@ -13,6 +13,6 @@
"repo": "anthropics/claude-code"
},
"installLocation": "/home/cal/.claude/plugins/marketplaces/claude-code-plugins",
"lastUpdated": "2026-02-20T04:08:05.428Z"
"lastUpdated": "2026-02-25T23:28:10.907Z"
}
}

@ -1 +1 @@
Subproject commit 0d996a7c346e2f3aca34e3488e49f1da537d7811
Subproject commit 76c0cbaeb563cd548b11898ba3892812f1ea510f

@ -1 +1 @@
Subproject commit 8deab8460a9d4df5a01315ef722a5ca6b061c074
Subproject commit 55b58ec6e5649104f926ba7558b567dc8d33c5ff

View File

@ -2,7 +2,8 @@
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"env": {
"MCP_API_KEY": "${MCP_API_KEY}",
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
"ENABLE_TOOL_SEARCH": "true"
},
"permissions": {
"allow": [
@ -56,7 +57,39 @@
"mcp__memorygraph__get_memory",
"Skill(notediscovery)",
"mcp__cognitive-memory__*",
"mcp__n8n-mcp__*"
"mcp__n8n-mcp__*",
"WebFetch(domain:custom-system-builder.gitlab.io)",
"WebFetch(domain:foundryvtt.com)",
"WebFetch(domain:10.10.0.174)",
"WebFetch(domain:docs.openclaw.ai)",
"WebFetch(domain:pve.proxmox.com)",
"WebFetch(domain:git.manticorum.com)",
"WebFetch(domain:developers.cloudflare.com)",
"WebFetch(domain:uptime-kuma-api.readthedocs.io)",
"WebFetch(domain:www.enworld.org)",
"WebFetch(domain:2minutetabletop.com)",
"WebFetch(domain:www.forgotten-adventures.net)",
"WebFetch(domain:www.doodlesanddragons.com)",
"WebFetch(domain:www.drivethrurpg.com)",
"WebFetch(domain:forums.rptools.net)",
"WebFetch(domain:www.fantasygrounds.com)",
"WebFetch(domain:gmkeros.wordpress.com)",
"WebFetch(domain:inkwellideas.com)",
"WebFetch(domain:itch.io)",
"WebFetch(domain:opengameart.org)",
"WebFetch(domain:store.paizo.com)",
"WebFetch(domain:forum.gitea.com)",
"WebFetch(domain:docs.gitea.com)",
"WebFetch(domain:gitea.com)",
"WebFetch(domain:docs.anthropic.com)",
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:ra-h.app)",
"WebFetch(domain:ollama.com)",
"mcp__cognitive-memory__memory_store",
"mcp__cognitive-memory__memory_episode",
"mcp__cognitive-memory__memory_recall",
"mcp__cognitive-memory__memory_search",
"mcp__cognitive-memory__memory_get"
],
"deny": [
"Bash(diskutil partitionDisk)",
@ -130,7 +163,8 @@
},
"enabledPlugins": {
"playground@claude-plugins-official": true,
"claude-code-setup@claude-plugins-official": true
"claude-code-setup@claude-plugins-official": true,
"frontend-design@claude-plugins-official": true
},
"skipDangerousModePermissionPrompt": true,
"effortLevel": "medium"

View File

@ -33,7 +33,12 @@ class PaperDynastyAPI:
api.wipe_team_cards(team_id=464)
"""
def __init__(self, environment: str = 'dev', token: Optional[str] = None, verbose: bool = False):
def __init__(
self,
environment: str = "dev",
token: Optional[str] = None,
verbose: bool = False,
):
"""
Initialize API client
@ -44,16 +49,16 @@ class PaperDynastyAPI:
"""
self.env = environment.lower()
self.base_url = (
'https://pd.manticorum.com/api'
if 'prod' in self.env
else 'https://pddev.manticorum.com/api'
"https://pd.manticorum.com/api"
if "prod" in self.env
else "https://pddev.manticorum.com/api"
)
self.token = token or os.getenv('API_TOKEN')
self.token = token or os.getenv("API_TOKEN")
self.verbose = verbose
self.headers = {'Content-Type': 'application/json'}
self.headers = {"Content-Type": "application/json"}
if self.token:
self.headers['Authorization'] = f'Bearer {self.token}'
self.headers["Authorization"] = f"Bearer {self.token}"
def _require_token(self):
"""Raise if no API token is set (needed for write operations)"""
@ -68,16 +73,22 @@ class PaperDynastyAPI:
if self.verbose:
print(f"[API] {message}")
def _build_url(self, endpoint: str, api_ver: int = 2, object_id: Optional[int] = None, params: Optional[List] = None) -> str:
def _build_url(
self,
endpoint: str,
api_ver: int = 2,
object_id: Optional[int] = None,
params: Optional[List] = None,
) -> str:
"""Build API URL with parameters"""
url = f'{self.base_url}/v{api_ver}/{endpoint}'
url = f"{self.base_url}/v{api_ver}/{endpoint}"
if object_id is not None:
url += f'/{object_id}'
url += f"/{object_id}"
if params:
param_strs = [f'{k}={v}' for k, v in params]
url += '?' + '&'.join(param_strs)
param_strs = [f"{k}={v}" for k, v in params]
url += "?" + "&".join(param_strs)
return url
@ -85,7 +96,13 @@ class PaperDynastyAPI:
# Low-level HTTP methods
# ====================
def get(self, endpoint: str, object_id: Optional[int] = None, params: Optional[List] = None, timeout: int = 10) -> Dict:
def get(
self,
endpoint: str,
object_id: Optional[int] = None,
params: Optional[List] = None,
timeout: int = 10,
) -> Dict:
"""GET request to API"""
url = self._build_url(endpoint, object_id=object_id, params=params)
self._log(f"GET {url}")
@ -93,16 +110,22 @@ class PaperDynastyAPI:
response.raise_for_status()
return response.json()
def post(self, endpoint: str, payload: Optional[Dict] = None, timeout: int = 10) -> Any:
def post(
self, endpoint: str, payload: Optional[Dict] = None, timeout: int = 10
) -> Any:
"""POST request to API"""
self._require_token()
url = self._build_url(endpoint)
self._log(f"POST {url}")
response = requests.post(url, headers=self.headers, json=payload, timeout=timeout)
response = requests.post(
url, headers=self.headers, json=payload, timeout=timeout
)
response.raise_for_status()
return response.json() if response.text else {}
def patch(self, endpoint: str, object_id: int, params: List, timeout: int = 10) -> Dict:
def patch(
self, endpoint: str, object_id: int, params: List, timeout: int = 10
) -> Dict:
"""PATCH request to API"""
self._require_token()
url = self._build_url(endpoint, object_id=object_id, params=params)
@ -124,7 +147,9 @@ class PaperDynastyAPI:
# Team Operations
# ====================
def get_team(self, team_id: Optional[int] = None, abbrev: Optional[str] = None) -> Dict:
def get_team(
self, team_id: Optional[int] = None, abbrev: Optional[str] = None
) -> Dict:
"""
Get a team by ID or abbreviation
@ -136,17 +161,19 @@ class PaperDynastyAPI:
Team dict
"""
if team_id:
return self.get('teams', object_id=team_id)
return self.get("teams", object_id=team_id)
elif abbrev:
result = self.get('teams', params=[('abbrev', abbrev.upper())])
teams = result.get('teams', [])
result = self.get("teams", params=[("abbrev", abbrev.upper())])
teams = result.get("teams", [])
if not teams:
raise ValueError(f"Team '{abbrev}' not found")
return teams[0]
else:
raise ValueError("Must provide team_id or abbrev")
def list_teams(self, season: Optional[int] = None, event_id: Optional[int] = None) -> List[Dict]:
def list_teams(
self, season: Optional[int] = None, event_id: Optional[int] = None
) -> List[Dict]:
"""
List teams
@ -159,12 +186,12 @@ class PaperDynastyAPI:
"""
params = []
if season:
params.append(('season', season))
params.append(("season", season))
if event_id:
params.append(('event', event_id))
params.append(("event", event_id))
result = self.get('teams', params=params if params else None)
return result.get('teams', [])
result = self.get("teams", params=params if params else None)
return result.get("teams", [])
# ====================
# Card Operations
@ -180,9 +207,11 @@ class PaperDynastyAPI:
Returns:
API response
"""
return self.post(f'cards/wipe-team/{team_id}')
return self.post(f"cards/wipe-team/{team_id}")
def list_cards(self, team_id: Optional[int] = None, player_id: Optional[int] = None) -> List[Dict]:
def list_cards(
self, team_id: Optional[int] = None, player_id: Optional[int] = None
) -> List[Dict]:
"""
List cards. At least one filter is required to avoid massive unfiltered queries.
@ -194,16 +223,18 @@ class PaperDynastyAPI:
List of card dicts
"""
if not team_id and not player_id:
raise ValueError("list_cards requires at least one filter (team_id or player_id)")
raise ValueError(
"list_cards requires at least one filter (team_id or player_id)"
)
params = []
if team_id:
params.append(('team_id', team_id))
params.append(("team_id", team_id))
if player_id:
params.append(('player_id', player_id))
params.append(("player_id", player_id))
result = self.get('cards', params=params if params else None)
return result.get('cards', [])
result = self.get("cards", params=params if params else None)
return result.get("cards", [])
# ====================
# Pack Operations
@ -215,7 +246,7 @@ class PaperDynastyAPI:
opened: Optional[bool] = None,
new_to_old: bool = False,
limit: Optional[int] = None,
timeout: int = 10
timeout: int = 10,
) -> List[Dict]:
"""
List packs
@ -241,20 +272,22 @@ class PaperDynastyAPI:
packs = api.list_packs(opened=True, limit=2000, timeout=30)
"""
if team_id is None and opened is None:
raise ValueError("list_packs requires at least one filter (team_id or opened)")
raise ValueError(
"list_packs requires at least one filter (team_id or opened)"
)
params = []
if team_id:
params.append(('team_id', team_id))
params.append(("team_id", team_id))
if opened is not None:
params.append(('opened', 'true' if opened else 'false'))
params.append(("opened", "true" if opened else "false"))
if new_to_old:
params.append(('new_to_old', 'true'))
params.append(("new_to_old", "true"))
if limit:
params.append(('limit', str(limit)))
params.append(("limit", str(limit)))
result = self.get('packs', params=params if params else None, timeout=timeout)
return result.get('packs', [])
result = self.get("packs", params=params if params else None, timeout=timeout)
return result.get("packs", [])
def delete_pack(self, pack_id: int) -> str:
"""
@ -266,10 +299,15 @@ class PaperDynastyAPI:
Returns:
Success message
"""
return self.delete('packs', object_id=pack_id)
return self.delete("packs", object_id=pack_id)
def update_pack(self, pack_id: int, pack_cardset_id: Optional[int] = None,
pack_team_id: Optional[int] = None, pack_type_id: Optional[int] = None) -> Dict:
def update_pack(
self,
pack_id: int,
pack_cardset_id: Optional[int] = None,
pack_team_id: Optional[int] = None,
pack_type_id: Optional[int] = None,
) -> Dict:
"""
Update pack properties (PATCH)
@ -288,13 +326,13 @@ class PaperDynastyAPI:
"""
params = []
if pack_cardset_id is not None:
params.append(('pack_cardset_id', pack_cardset_id))
params.append(("pack_cardset_id", pack_cardset_id))
if pack_team_id is not None:
params.append(('pack_team_id', pack_team_id))
params.append(("pack_team_id", pack_team_id))
if pack_type_id is not None:
params.append(('pack_type_id', pack_type_id))
params.append(("pack_type_id", pack_type_id))
return self.patch('packs', object_id=pack_id, params=params)
return self.patch("packs", object_id=pack_id, params=params)
def create_packs(self, packs: List[Dict]) -> Any:
"""
@ -313,8 +351,8 @@ class PaperDynastyAPI:
for _ in range(5)
])
"""
payload = {'packs': packs}
return self.post('packs', payload=payload)
payload = {"packs": packs}
return self.post("packs", payload=payload)
def get_packs_opened_today(self, limit: int = 2000, timeout: int = 30) -> Dict:
"""
@ -338,57 +376,67 @@ class PaperDynastyAPI:
from collections import defaultdict
# Get recent opened packs
packs = self.list_packs(opened=True, new_to_old=True, limit=limit, timeout=timeout)
packs = self.list_packs(
opened=True, new_to_old=True, limit=limit, timeout=timeout
)
# Today's date (UTC)
today = datetime.now(timezone.utc).date()
# Count packs by team
teams_data = defaultdict(lambda: {'count': 0, 'abbrev': '', 'lname': '', 'first': None, 'last': None})
teams_data = defaultdict(
lambda: {"count": 0, "abbrev": "", "lname": "", "first": None, "last": None}
)
total = 0
for pack in packs:
if pack.get('open_time'):
if pack.get("open_time"):
try:
open_dt = datetime.fromtimestamp(pack['open_time'] / 1000, tz=timezone.utc)
open_dt = datetime.fromtimestamp(
pack["open_time"] / 1000, tz=timezone.utc
)
if open_dt.date() == today:
total += 1
team_id = pack['team']['id']
teams_data[team_id]['abbrev'] = pack['team']['abbrev']
teams_data[team_id]['lname'] = pack['team']['lname']
teams_data[team_id]['count'] += 1
team_id = pack["team"]["id"]
teams_data[team_id]["abbrev"] = pack["team"]["abbrev"]
teams_data[team_id]["lname"] = pack["team"]["lname"]
teams_data[team_id]["count"] += 1
if teams_data[team_id]['first'] is None or open_dt < teams_data[team_id]['first']:
teams_data[team_id]['first'] = open_dt
if teams_data[team_id]['last'] is None or open_dt > teams_data[team_id]['last']:
teams_data[team_id]['last'] = open_dt
if (
teams_data[team_id]["first"] is None
or open_dt < teams_data[team_id]["first"]
):
teams_data[team_id]["first"] = open_dt
if (
teams_data[team_id]["last"] is None
or open_dt > teams_data[team_id]["last"]
):
teams_data[team_id]["last"] = open_dt
except Exception:
pass
# Format results
teams_list = []
for team_id, data in teams_data.items():
teams_list.append({
'team_id': team_id,
'abbrev': data['abbrev'],
'name': data['lname'],
'packs': data['count'],
'first_pack': data['first'].isoformat() if data['first'] else None,
'last_pack': data['last'].isoformat() if data['last'] else None
})
teams_list.append(
{
"team_id": team_id,
"abbrev": data["abbrev"],
"name": data["lname"],
"packs": data["count"],
"first_pack": data["first"].isoformat() if data["first"] else None,
"last_pack": data["last"].isoformat() if data["last"] else None,
}
)
# Sort by pack count
teams_list.sort(key=lambda x: x['packs'], reverse=True)
teams_list.sort(key=lambda x: x["packs"], reverse=True)
result = {
'total': total,
'teams': teams_list,
'date': today.isoformat()
}
result = {"total": total, "teams": teams_list, "date": today.isoformat()}
if len(packs) == limit:
result['note'] = f'Hit limit of {limit} packs - actual count may be higher'
result["note"] = f"Hit limit of {limit} packs - actual count may be higher"
return result
@ -397,7 +445,8 @@ class PaperDynastyAPI:
num_packs: int = 5,
exclude_team_abbrev: Optional[List[str]] = None,
pack_type_id: int = 1,
season: Optional[int] = None
season: Optional[int] = None,
cardset_id: Optional[int] = None,
) -> Dict:
"""
Distribute packs to all human-controlled teams
@ -407,6 +456,7 @@ class PaperDynastyAPI:
exclude_team_abbrev: List of team abbreviations to exclude (default: None)
pack_type_id: Pack type ID (default: 1 = Standard packs)
season: Season to distribute for (default: current season)
cardset_id: Cardset ID for pack types that require it (e.g., Promo Choice = type 9)
Returns:
Dict with keys:
@ -429,8 +479,8 @@ class PaperDynastyAPI:
# Get current season if not specified
if season is None:
current = self.get('current')
season = current['season']
current = self.get("current")
season = current["season"]
self._log(f"Distributing {num_packs} packs to season {season} teams")
@ -440,9 +490,9 @@ class PaperDynastyAPI:
# Filter for human-controlled teams only
qualifying_teams = []
for team in all_teams:
if not team['is_ai'] and 'gauntlet' not in team['abbrev'].lower():
if not team["is_ai"] and "gauntlet" not in team["abbrev"].lower():
# Check if team is in exclusion list
if team['abbrev'].upper() in exclude_team_abbrev:
if team["abbrev"].upper() in exclude_team_abbrev:
self._log(f"Excluding team {team['abbrev']}: {team['sname']}")
continue
qualifying_teams.append(team)
@ -457,27 +507,34 @@ class PaperDynastyAPI:
self._log(f"Giving {num_packs} packs to {team['abbrev']} ({team['sname']})")
# Create pack payload
packs = [{
'team_id': team['id'],
'pack_type_id': pack_type_id,
'pack_cardset_id': None
} for _ in range(num_packs)]
packs = [
{
"team_id": team["id"],
"pack_type_id": pack_type_id,
"pack_cardset_id": cardset_id,
}
for _ in range(num_packs)
]
try:
self.create_packs(packs)
total_packs += num_packs
self._log(f" ✓ Successfully gave {num_packs} packs to {team['abbrev']}")
self._log(
f" ✓ Successfully gave {num_packs} packs to {team['abbrev']}"
)
except Exception as e:
self._log(f" ✗ Failed to give packs to {team['abbrev']}: {e}")
raise
result = {
'total_packs': total_packs,
'teams_count': len(qualifying_teams),
'teams': qualifying_teams
"total_packs": total_packs,
"teams_count": len(qualifying_teams),
"teams": qualifying_teams,
}
self._log(f"Distribution complete: {total_packs} packs to {len(qualifying_teams)} teams")
self._log(
f"Distribution complete: {total_packs} packs to {len(qualifying_teams)} teams"
)
return result
@ -485,7 +542,12 @@ class PaperDynastyAPI:
# Gauntlet Operations
# ====================
def list_gauntlet_runs(self, event_id: Optional[int] = None, team_id: Optional[int] = None, active_only: bool = False) -> List[Dict]:
def list_gauntlet_runs(
self,
event_id: Optional[int] = None,
team_id: Optional[int] = None,
active_only: bool = False,
) -> List[Dict]:
"""
List gauntlet runs
@ -499,14 +561,14 @@ class PaperDynastyAPI:
"""
params = []
if event_id:
params.append(('gauntlet_id', event_id))
params.append(("gauntlet_id", event_id))
if team_id:
params.append(('team_id', team_id))
params.append(("team_id", team_id))
if active_only:
params.append(('is_active', 'true'))
params.append(("is_active", "true"))
result = self.get('gauntletruns', params=params if params else None)
return result.get('runs', [])
result = self.get("gauntletruns", params=params if params else None)
return result.get("runs", [])
def end_gauntlet_run(self, run_id: int) -> Dict:
"""
@ -518,7 +580,7 @@ class PaperDynastyAPI:
Returns:
Updated run dict
"""
return self.patch('gauntletruns', object_id=run_id, params=[('ended', 'true')])
return self.patch("gauntletruns", object_id=run_id, params=[("ended", "true")])
# ====================
# Player Operations
@ -534,9 +596,14 @@ class PaperDynastyAPI:
Returns:
Player dict
"""
return self.get('players', object_id=player_id)
return self.get("players", object_id=player_id)
def list_players(self, cardset_id: Optional[int] = None, rarity: Optional[str] = None, timeout: int = 30) -> List[Dict]:
def list_players(
self,
cardset_id: Optional[int] = None,
rarity: Optional[str] = None,
timeout: int = 30,
) -> List[Dict]:
"""
List players. At least one filter is required to avoid massive unfiltered queries.
@ -549,22 +616,26 @@ class PaperDynastyAPI:
List of player dicts
"""
if not cardset_id and not rarity:
raise ValueError("list_players requires at least one filter (cardset_id or rarity)")
raise ValueError(
"list_players requires at least one filter (cardset_id or rarity)"
)
params = []
if cardset_id:
params.append(('cardset', cardset_id))
params.append(("cardset", cardset_id))
if rarity:
params.append(('rarity', rarity))
params.append(("rarity", rarity))
result = self.get('players', params=params, timeout=timeout)
return result.get('players', [])
result = self.get("players", params=params, timeout=timeout)
return result.get("players", [])
# ====================
# Result/Stats Operations
# ====================
def list_results(self, season: Optional[int] = None, team_id: Optional[int] = None) -> List[Dict]:
def list_results(
self, season: Optional[int] = None, team_id: Optional[int] = None
) -> List[Dict]:
"""
List game results. At least one filter is required to avoid massive unfiltered queries.
@ -576,22 +647,26 @@ class PaperDynastyAPI:
List of result dicts
"""
if not season and not team_id:
raise ValueError("list_results requires at least one filter (season or team_id)")
raise ValueError(
"list_results requires at least one filter (season or team_id)"
)
params = []
if season:
params.append(('season', season))
params.append(("season", season))
if team_id:
params.append(('team_id', team_id))
params.append(("team_id", team_id))
result = self.get('results', params=params if params else None)
return result.get('results', [])
result = self.get("results", params=params if params else None)
return result.get("results", [])
# ====================
# Helper Methods
# ====================
def find_gauntlet_teams(self, event_id: Optional[int] = None, active_only: bool = False) -> List[Dict]:
def find_gauntlet_teams(
self, event_id: Optional[int] = None, active_only: bool = False
) -> List[Dict]:
"""
Find gauntlet teams (teams with 'Gauntlet' in abbrev)
@ -607,22 +682,22 @@ class PaperDynastyAPI:
runs = self.list_gauntlet_runs(event_id=event_id, active_only=True)
teams_with_runs = []
for run in runs:
team = run['team']
team['active_run'] = run
team = run["team"]
team["active_run"] = run
teams_with_runs.append(team)
return teams_with_runs
else:
# Get all teams with 'Gauntlet' in name
all_teams = self.list_teams()
gauntlet_teams = [t for t in all_teams if 'Gauntlet' in t.get('abbrev', '')]
gauntlet_teams = [t for t in all_teams if "Gauntlet" in t.get("abbrev", "")]
# Optionally add run info
if event_id:
runs = self.list_gauntlet_runs(event_id=event_id)
run_by_team = {r['team']['id']: r for r in runs}
run_by_team = {r["team"]["id"]: r for r in runs}
for team in gauntlet_teams:
if team['id'] in run_by_team:
team['run'] = run_by_team[team['id']]
if team["id"] in run_by_team:
team["run"] = run_by_team[team["id"]]
return gauntlet_teams
@ -631,9 +706,11 @@ def main():
"""Example usage"""
import argparse
parser = argparse.ArgumentParser(description='Paper Dynasty API Client')
parser.add_argument('--env', choices=['prod', 'dev'], default='dev', help='Environment')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
parser = argparse.ArgumentParser(description="Paper Dynasty API Client")
parser.add_argument(
"--env", choices=["prod", "dev"], default="dev", help="Environment"
)
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
args = parser.parse_args()
try:
@ -650,5 +727,5 @@ def main():
sys.exit(1)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -56,6 +56,7 @@ console = Console()
class State:
"""Global state for API client and settings"""
api: Optional[PaperDynastyAPI] = None
json_output: bool = False
@ -67,14 +68,19 @@ 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):
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)
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:
@ -90,7 +96,9 @@ def handle_error(e: Exception, context: str = ""):
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.")
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)
@ -100,11 +108,16 @@ def handle_error(e: Exception, context: str = ""):
# Main Callback (Global Options)
# ============================================================================
@app.callback()
def main(
env: Annotated[str, typer.Option("--env", help="Environment: prod or dev")] = "prod",
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,
verbose: Annotated[
bool, typer.Option("--verbose", "-v", help="Verbose output")
] = False,
):
"""Paper Dynasty Baseball Card Game CLI"""
state.api = PaperDynastyAPI(environment=env, verbose=verbose)
@ -115,6 +128,7 @@ def main(
# Status & Health Commands
# ============================================================================
@app.command()
def status():
"""Show packs opened today summary"""
@ -125,18 +139,20 @@ def status():
output_json(result)
return
console.print(f"\n[bold cyan]Packs Opened Today ({result['date']})[/bold cyan]\n")
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']:
if result["teams"]:
rows = []
for t in result['teams']:
rows.append([t['abbrev'], t['name'], t['packs']])
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'):
if result.get("note"):
console.print(f"\n[yellow]Note:[/yellow] {result['note']}")
except Exception as e:
@ -159,9 +175,12 @@ def health():
# Team Commands
# ============================================================================
@team_app.command("list")
def team_list(
season: Annotated[Optional[int], typer.Option("--season", "-s", help="Filter by season")] = None,
season: Annotated[
Optional[int], typer.Option("--season", "-s", help="Filter by season")
] = None,
):
"""List all teams"""
try:
@ -176,23 +195,27 @@ def team_list(
return
# Filter out gauntlet teams for cleaner display
regular_teams = [t for t in teams if 'Gauntlet' not in t.get('abbrev', '')]
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'
])
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)
output_table(
title, ["Abbrev", "Name", "Season", "Wallet", "Rank", "Type"], rows
)
except Exception as e:
handle_error(e)
@ -239,7 +262,7 @@ def team_cards(
"""List team's cards"""
try:
team = state.api.get_team(abbrev=abbrev.upper())
cards = state.api.list_cards(team_id=team['id'])
cards = state.api.list_cards(team_id=team["id"])
if state.json_output:
output_json(cards)
@ -251,22 +274,26 @@ def team_cards(
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)
])
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
rows,
)
if len(cards) > limit:
console.print(f"\n[dim]Showing {limit} of {len(cards)} cards. Use --limit to see more.[/dim]")
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}")
@ -279,10 +306,16 @@ def team_cards(
# 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,
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"""
@ -291,10 +324,12 @@ def pack_list(
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)
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)
packs = state.api.list_packs(
team_id=team_id, opened=opened, new_to_old=True, limit=limit
)
if state.json_output:
output_json(packs)
@ -306,15 +341,17 @@ def pack_list(
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
])
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:
@ -342,30 +379,54 @@ def pack_today():
@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="1=Standard, 2=Starter, 3=Premium, 4=Check-In, 5=MVP, 6=All Star, 7=Mario, 8=Team Choice, 9=Promo Choice")] = 1,
dry_run: Annotated[bool, typer.Option("--dry-run", help="Show what would be done")] = False,
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="1=Standard, 2=Starter, 3=Premium, 4=Check-In, 5=MVP, 6=All Star, 7=Mario, 8=Team Choice, 9=Promo Choice",
),
] = 1,
cardset: Annotated[
Optional[int],
typer.Option(
"--cardset", "-c", help="Cardset ID (required for Promo Choice packs)"
),
] = None,
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']
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
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"\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}")
if cardset is not None:
console.print(f"[bold]Cardset ID:[/bold] {cardset}")
console.print(f"[bold]Teams:[/bold] {len(qualifying)}")
console.print(f"[bold]Total packs:[/bold] {num * len(qualifying)}")
@ -381,7 +442,8 @@ def pack_distribute(
result = state.api.distribute_packs(
num_packs=num,
exclude_team_abbrev=exclude,
pack_type_id=pack_type
pack_type_id=pack_type,
cardset_id=cardset,
)
if state.json_output:
@ -403,10 +465,15 @@ def pack_distribute(
# 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,
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:
@ -422,17 +489,19 @@ def gauntlet_list(
rows = []
for r in runs:
team = r.get('team', {})
is_active = "Active" if r.get('ended') is None else "Ended"
gauntlet = r.get('gauntlet', {})
rows.append([
r['id'],
team.get('abbrev', 'N/A'),
r.get('wins', 0),
r.get('losses', 0),
gauntlet.get('id', 'N/A'),
is_active
])
team = r.get("team", {})
is_active = "Active" if r.get("ended") is None else "Ended"
gauntlet = r.get("gauntlet", {})
rows.append(
[
r["id"],
team.get("abbrev", "N/A"),
r.get("wins", 0),
r.get("losses", 0),
gauntlet.get("id", "N/A"),
is_active,
]
)
title = "Gauntlet Runs"
if event_id:
@ -448,8 +517,12 @@ def gauntlet_list(
@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,
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:
@ -465,16 +538,10 @@ def gauntlet_teams(
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
])
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:
@ -488,29 +555,37 @@ def gauntlet_teams(
@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)")],
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']
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)
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'}")
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]")
@ -527,13 +602,13 @@ def gauntlet_cleanup(
# 2. Delete packs
for pack in packs:
state.api.delete_pack(pack['id'])
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'])
state.api.end_gauntlet_run(active_run["id"])
results.append(f"Ended run {active_run['id']}")
console.print(f"\n[green]Cleanup complete![/green]")
@ -551,6 +626,7 @@ def gauntlet_cleanup(
# Player Commands
# ============================================================================
@player_app.command("get")
def player_get(
player_id: Annotated[int, typer.Argument(help="Player ID")],
@ -566,13 +642,13 @@ def player_get(
# Get positions
positions = []
for i in range(1, 9):
pos = player.get(f'pos_{i}')
pos = player.get(f"pos_{i}")
if pos:
positions.append(pos)
cardset = player.get('cardset', {})
rarity = player.get('rarity', {})
rarity_name = rarity.get('name', 'N/A') if isinstance(rarity, dict) else rarity
cardset = player.get("cardset", {})
rarity = player.get("rarity", {})
rarity_name = rarity.get("name", "N/A") if isinstance(rarity, dict) else rarity
panel = Panel(
f"[bold]ID:[/bold] {player['player_id']}\n"
@ -593,9 +669,15 @@ def player_get(
@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,
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:
@ -611,16 +693,18 @@ def player_list(
rows = []
for p in players[:limit]:
cs = p.get('cardset', {})
rarity = p.get('rarity', {})
rarity_name = rarity.get('name', '') if isinstance(rarity, dict) else rarity
rows.append([
p['player_id'],
p.get('p_name', 'Unknown'),
rarity_name,
p.get('cost', 0),
cs.get('name', 'N/A')
])
cs = p.get("cardset", {})
rarity = p.get("rarity", {})
rarity_name = rarity.get("name", "") if isinstance(rarity, dict) else rarity
rows.append(
[
p["player_id"],
p.get("p_name", "Unknown"),
rarity_name,
p.get("cost", 0),
cs.get("name", "N/A"),
]
)
title = "Players"
if rarity:
@ -631,7 +715,9 @@ def player_list(
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]")
console.print(
f"\n[dim]Showing {limit} of {len(players)} players. Use --limit to see more.[/dim]"
)
except Exception as e:
handle_error(e)

View File

@ -7,6 +7,7 @@ Works with both production and development environments.
This script uses the Paper Dynasty API client for all operations.
"""
import argparse
import logging
import os
@ -18,23 +19,31 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
from api_client import PaperDynastyAPI
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger('pack_distribution')
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("pack_distribution")
def distribute_packs(num_packs: int = 5, exclude_team_abbrev: list[str] = None, pack_type_id: int = 1):
def distribute_packs(
num_packs: int = 5,
exclude_team_abbrev: list[str] = None,
pack_type_id: int = 1,
cardset_id: int = None,
):
"""Distribute packs to all human-controlled teams using Paper Dynasty API client
Args:
num_packs: Number of packs to give to each team (default: 5)
exclude_team_abbrev: List of team abbreviations to exclude (default: None)
pack_type_id: Pack type ID (default: 1 = Standard packs)
cardset_id: Cardset ID for pack types that require it (e.g., Promo Choice = type 9)
"""
if exclude_team_abbrev is None:
exclude_team_abbrev = []
# Get environment
database_env = os.getenv('DATABASE', 'dev').lower()
database_env = os.getenv("DATABASE", "dev").lower()
try:
# Initialize API client
@ -44,11 +53,14 @@ def distribute_packs(num_packs: int = 5, exclude_team_abbrev: list[str] = None,
result = api.distribute_packs(
num_packs=num_packs,
exclude_team_abbrev=exclude_team_abbrev,
pack_type_id=pack_type_id
pack_type_id=pack_type_id,
cardset_id=cardset_id,
)
# Log final summary
logger.info(f"\n🎉 All done! Distributed {result['total_packs']} packs to {result['teams_count']} teams")
logger.info(
f"\n🎉 All done! Distributed {result['total_packs']} packs to {result['teams_count']} teams"
)
except ValueError as e:
logger.error(f"Configuration error: {e}")
@ -58,9 +70,9 @@ def distribute_packs(num_packs: int = 5, exclude_team_abbrev: list[str] = None,
sys.exit(1)
if __name__ == '__main__':
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description='Distribute packs to all human-controlled teams',
description="Distribute packs to all human-controlled teams",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
@ -85,25 +97,31 @@ Examples:
Environment Variables:
API_TOKEN - Required: API authentication token
DATABASE - Optional: 'dev' (default) or 'prod'
"""
""",
)
parser.add_argument(
'--num-packs',
"--num-packs",
type=int,
default=5,
help='Number of packs to give to each team (default: 5)'
help="Number of packs to give to each team (default: 5)",
)
parser.add_argument(
'--exclude-team-abbrev',
nargs='*',
"--exclude-team-abbrev",
nargs="*",
default=[],
help='Team abbreviations to exclude (space-separated, e.g., NYY BOS LAD)'
help="Team abbreviations to exclude (space-separated, e.g., NYY BOS LAD)",
)
parser.add_argument(
'--pack-type-id',
"--pack-type-id",
type=int,
default=1,
help='Pack type ID (default: 1 = Standard packs)'
help="Pack type ID (default: 1 = Standard packs)",
)
parser.add_argument(
"--cardset-id",
type=int,
default=None,
help="Cardset ID for pack types that require it (e.g., Promo Choice = type 9)",
)
args = parser.parse_args()
@ -111,5 +129,6 @@ Environment Variables:
distribute_packs(
num_packs=args.num_packs,
exclude_team_abbrev=args.exclude_team_abbrev,
pack_type_id=args.pack_type_id
pack_type_id=args.pack_type_id,
cardset_id=args.cardset_id,
)

@ -0,0 +1 @@
Subproject commit 477341020c40fdb547ac2e4b50fa8e22cd36547b