
pokecli: un CLI pequeño y tipado para PokeAPI
CLI de Python tipado y pensado para PokeAPI. Se instala con uv, usa caché local con TinyDB y queda listo para agentes gracias a un skill de Claude Code incluido en el paquete.
Descripción del proyecto
pokecli es una herramienta de línea de comandos de código abierto que construí para consultar datos de Pokémon, bayas, objetos, movimientos, habilidades, naturalezas, tipos, cadenas de evolución y especies desde PokeAPI. Es deliberadamente pequeña y es una de las implementaciones de referencia que suelo mostrar cuando hablo de cómo diseño una herramienta de línea de comandos. El proyecto también me sirve como ejemplo de implementación para herramientas nativas para IA: pokecli incluye un SKILL.md que un agente como Claude Code o Copilot puede cargar para aprender el conjunto de comandos sin tener que releer --help en cada tarea.

Tecnologías utilizadas
| Capa | Elección | Por qué forma parte del proyecto |
|---|---|---|
| Lenguaje | Python 3.12+ | Tipado moderno y pattern matching estructural |
| Framework de CLI | Typer | Subaplicaciones modulares, anotaciones de tipo nativas e inyección de contexto limpia |
| Modelos de datos | Pydantic v2 | Validación estricta cuando hace falta y extra="ignore" en todos los modelos |
| Cliente HTTP | httpx | Un ciclo de vida claro con context manager y margen para una versión async más adelante |
| Caché local | TinyDB | Un único archivo JSON, una tabla por recurso y un formato fácil de inspeccionar |
| Interfaz de terminal | Rich | Tablas, paneles, resaltado de sintaxis para JSON y alternativas en ASCII |
| Empaquetado | uv + uv_build | Resolución rápida de dependencias, estructura moderna con src/ y [project.scripts] |
| Pruebas | pytest | Fixture tmp_path para la caché y un archivo de pruebas de modelos por recurso |
| Herramientas de desarrollo | ruff, pre-commit | Formato, linting y una base mínima de higiene para los commits |
Arquitectura
pokecli está organizado en cinco capas, cada una a cargo de exactamente una cosa. Los comandos conocen Typer. El cliente del API conoce httpx. El caché conoce TinyDB. Los modelos conocen Pydantic. La capa de presentación conoce Rich. Ninguna capa invade el carril de otra.
flowchart LR
subgraph CLI["Composición con Typer"]
MAIN["main.py\napp raíz"]
DOMAIN_CMDS["commands/\npokemon.py\nberry.py\nimage.py\n..."]
CACHE_CMD["commands/cache.py"]
end
subgraph CORE["Núcleo compartido"]
UTILS["commands/_utils.py"]
CLIENT["api/client.py"]
STORE["cache/store.py"]
end
subgraph DATA["Dominio y presentación"]
MODELS["models/*.py"]
RENDER["display/*.py"]
end
MAIN --> DOMAIN_CMDS
MAIN --> CACHE_CMD
DOMAIN_CMDS --> UTILS
DOMAIN_CMDS --> MODELS
DOMAIN_CMDS --> RENDER
UTILS --> CLIENT
UTILS --> STORE
CACHE_CMD --> STORE
Cada comando get sigue el mismo flujo corto. Lo imponen los límites entre módulos, no una convención informal.
flowchart LR
A([Solicitud]) --> B{¿Hit en caché?}
B -- sí --> C[Datos en caché]
B -- no --> D[Consulta al API]
D --> E[Actualiza caché]
C --> F[Transforma la respuesta]
E --> F
F --> G{Modo de salida}
G -- tabla --> H[Vista de tabla]
G -- json --> I[Vista JSON]
Estructura del paquete de un vistazo
src/pokecli/
├── main.py # root Typer app, sub-apps registered here
├── config.py # POKEAPI_BASE_URL, DEFAULT_LIMIT, CACHE_DB_PATH
├── api/client.py # PokeAPIClient (httpx + context manager)
├── cache/store.py # CacheStore (TinyDB, table per resource)
├── commands/
│ ├── _utils.py # fetch_resource, fetch_list
│ ├── pokemon.py # get / moves / species / evolution / list
│ ├── berry.py item.py move.py ability.py nature.py type.py
│ ├── image.py # download (sprite variants)
│ ├── cache.py # stats / clear
│ └── install.py # install --skills
├── display/ # Rich renderers, one file per resource
├── models/ # Pydantic v2 models, one file per resource
└── skills/pokecli/
├── SKILL.md
└── references/api-fields.md
Funcionalidades clave
Estas funcionalidades muestran el conjunto de capacidades de pokecli y el tipo de experiencia de línea de comandos que busco construir: pequeña, tipada, scriptable y clara de entender a primera vista.
Consultas de recursos con salida tipada
Cada recurso (pokemon, berry, item, move, ability, nature, type) tiene comandos get y list paralelos con flags compartidos (--no-cache, --format). Los comandos especializados de Pokémon agregan moves, species y evolution.

Descarga de sprites
pokecli image download pokemon <name_or_id> -o <path> guarda los sprites localmente. El flag --variant selecciona entre seis vistas conocidas.
Caché local con control por recurso
La primera llamada va a la red; a partir de ahí, cada consulta se sirve desde ~/.pokecli/cache.json. Puedes inspeccionar o limpiar la caché con granularidad por recurso.

Desafíos técnicos
Esta sección muestra las decisiones y compensaciones de ingeniería detrás de pokecli. Cada tarjeta destaca un problema que tuve que resolver, la decisión de diseño que tomé y lo que esa decisión dice sobre cómo construyo herramientas de línea de comandos.
1. Hacer que cada recurso sea una aplicación autocontenida de Typer
main.py monolítico termina convirtiéndose en el cuello de botella de cualquier cambio.commands/ expone su propio app = typer.Typer(...). main.py se reduce a diez líneas de imports y llamadas a app.add_typer(...). Cuando después agregué Habilidades, Naturalezas y Tipos, el coste fue un archivo nuevo y una línea más por cada recurso.2. Usar el contexto de Typer como punto de apoyo para inyectar dependencias
El callback raíz guarda el cliente HTTP como un recurso administrado dentro de typer.Context:
@app.callback()
def root(ctx: typer.Context) -> None:
ctx.ensure_object(dict)
ctx.obj["client"] = ctx.with_resource(PokeAPIClient())
ctx.with_resource se encarga de cerrar el cliente correctamente cuando Typer termina de ejecutar el comando. En las pruebas, sustituirlo por un cliente falso requiere una sola línea.
3. Centralizar en una sola utilidad el flujo de caché primero y red después
commands/_utils.py concentra ese flujo en un solo punto. Todos los comandos llaman a fetch_resource o fetch_list. Si más adelante añado reintentos, límites de ritmo o una opción de trazas, sé que lo resolveré tocando una única función.
def fetch_resource(client, resource, name_or_id, no_cache, err_console):
with CacheStore() as cache:
key = name_or_id.lower()
data = None if no_cache else cache.get(resource, key)
if data is None:
try:
data = client.get_resource(resource, name_or_id)
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
err_console.print(f"[red]Not found: '{name_or_id}'[/red]")
else:
err_console.print(f"[red]API error: {e.response.status_code}[/red]")
raise typer.Exit(1)
except (httpx.ConnectError, httpx.TimeoutException):
err_console.print("[red]Network error: could not reach PokeAPI[/red]")
raise typer.Exit(1)
cache.set(resource, key, data)
return data
4. Usar Pydantic como contrato frente a la API externa
ConfigDict(extra="ignore"). Los campos obligatorios se validan de forma estricta. Si falta uno, se lanza un ValidationError y el comando lo traduce a un mensaje claro junto con typer.Exit(2). Los códigos de salida ayudan a identificar en qué capa ocurrió el fallo.| Código de salida | Significado |
|---|---|
0 | Éxito |
1 | Fallo esperado (no encontrado, red caída) |
2 | Fallo de esquema (la respuesta del API no coincidió con el modelo) |
Un script de shell que envuelva pokecli puede actuar sobre esto.
5. Elegir TinyDB en lugar de SQLite o Redis
cat, borrar con rm o comparar con un diff. No sería mi elección para un servicio, pero en un CLI el coste encaja bien, y la separación por recurso permite que pokecli cache clear --resource pokemon actúe solo sobre esa parte.6. Dejar Rich aislado en la capa de presentación
display/. display/common.py reúne METHOD_COLORS, TYPE_COLORS, la detección de uses_unicode y el helper get_chars con alternativa en ASCII. render_json y render_list resuelven lo común. Ningún renderizador llama al API y ningún modelo importa Rich.7. Incluir el SKILL.md dentro del paquete
SKILL.md y su references/api-fields.md viajan dentro del wheel, en pokecli/skills/pokecli/. install.py usa importlib.resources para copiarlos a ~/.claude/skills/pokecli/, así que la capa pensada para agentes se entrega junto con el CLI y no como documentación separada.
Mantengo el skill pequeño a propósito y lo separé en tres capas que un agente puede cargar con poco contexto:
- el frontmatter es la capa de disparo, con el nombre del skill, una descripción corta y el límite de
allowed-tools - el cuerpo de
SKILL.mdes la guía de comandos de trabajo, organizada alrededor de los mismos grupos de comandos que ya expone el CLI references/api-fields.mdguarda el detalle a nivel de campos que solo hace falta cuando un agente necesita más profundidad
Esa estructura hace que la implementación sea nativa para IA sin convertirla en una segunda interfaz. El skill empaquetado refleja la superficie real de comandos, se instala con un solo comando y les da a Claude Code o Copilot el contexto suficiente para actuar sobre pokecli sin releer --help en cada tarea.
Qué demuestra este proyecto
| Área | Lo que muestra el repositorio |
|---|---|
| Empaquetado de Python | Estructura src/, uv_build, una entrada funcional en [project.scripts] y importlib.resources para incluir archivos auxiliares en el paquete |
| Arquitectura de CLI | Una composición con Typer donde cada recurso vive en su propia subaplicación, la inyección de dependencias pasa por el contexto y el acceso a la caché y a HTTP se resuelve en un único punto |
| Modelado de dominio | Pydantic v2 como capa de protección frente al API, campos obligatorios estrictos con extra="ignore" y model_dump() como base única para la salida en JSON |
| Manejo de errores | Códigos de salida distintos según el tipo de fallo, mensajes claros en stderr y una salida en stdout que sigue siendo fácil de consumir desde scripts |
| Caché simple y útil | Un caché local con TinyDB que no requiere infraestructura extra, organizado por recurso y que se puede omitir cuando hace falta con --no-cache |
| UX en terminal | Tablas y paneles de Rich, barras de estadísticas en Unicode con alternativa en ASCII y un uso consistente del color entre recursos |
| Criterio para las pruebas | Un archivo de pruebas de modelos por recurso, pruebas de contrato para la caché, un fixture tmp_cache y sin depender de asserts frágiles sobre la capa de Rich |
| Diseño nativo para IA | Un SKILL.md que viaja dentro del paquete, se instala con un solo comando en ~/.claude/skills/pokecli/ y se activa desde el frontmatter |
En conjunto, estas decisiones muestran por qué pokecli funciona bien como proyecto de referencia: el empaquetado es claro, los límites entre capas están bien definidos, los fallos están pensados y la herramienta sigue siendo lo bastante pequeña como para entenderse rápido sin perder rigor.
Resultados
pokecli es la pieza de código más pequeña que puedo mostrar y que aun así refleja todos los hábitos que aplico en un servicio de producción con interfaz de línea de comandos.
| Resultado | Evidencia en el repositorio |
|---|---|
| Listo para agentes en un solo comando | Después de instalarlo con uv tool install desde el repositorio, o con uv tool install git+https://github.com/jebucaro/pokecli, pokecli install --skills copia SKILL.md a ~/.claude/skills/pokecli/. En la siguiente sesión, Claude Code ya reconoce los subcomandos sin depender de prompts afinados a mano. |
| Respuestas casi instantáneas después de la primera consulta | Con la caché de TinyDB ya cargada, un comando como pokemon get charizard deja de depender de una petición a la red y pasa a resolverse leyendo JSON local. Una vez cargada, también puede usarse sin conexión. |
| Un recurso equivale a un archivo por capa | Agregar un nuevo recurso de PokeAPI sigue un patrón claro de tres archivos: commands/<resource>.py, models/<resource>.py y display/<resource>.py. No hace falta tocar un registro central ni descubrir dependencias escondidas. |
| Códigos de salida pensados para automatización | El contrato documentado de 0 / 1 / 2 permite usar pokecli desde scripts de shell o pipelines de CI sin tener que interpretar stderr. Los errores esperados y los problemas de esquema quedan claramente diferenciados. |
| Suite de pruebas determinista | Los modelos de Pydantic y la capa de caché están cubiertos de extremo a extremo, con un fixture tmp_path que evita tocar tanto ~/.pokecli/ como el API real durante las pruebas. |
| Sin infraestructura innecesaria | No hace falta un servidor de base de datos, un daemon de configuración ni depender de servicios en la nube. Con uv tool install desde Git, o con uv sync dentro de un clon del repo, queda listo para Linux, macOS y Windows. |
| Contrato tipado con un API externo | Nueve recursos (pokemon, berry, item, move, ability, nature, type, image y cache) se validan con Pydantic v2, lo que protege a quien lo usa frente a cambios inesperados en el esquema del API. |
Tan importante como leer el código es entender la experiencia que produce. pokecli toma una API pública y la convierte en una herramienta serena y automatizable, y el SKILL.md que acompaña al proyecto hace que ese CLI sea nativo para IA al enseñarle a un agente cómo usarlo incluso si el modelo nunca fue entrenado específicamente con pokecli. Así, el proyecto mantiene una huella pequeña para quien llega por primera vez y, al mismo tiempo, sigue siendo legible para un agente desde la siguiente sesión.
Foto de Jay ➡️ en Unsplash ➡️
Pokémon y los nombres de los personajes de Pokémon son marcas registradas de Nintendo.

