Saltar al contenido principal

Strava CLI con IA: tu analista de entrenamiento en terminal

10 min lectura Actualizado el 29 de abril de 2026
Cómo combinar Strava, TypeScript y LLMs para generar análisis de entrenamiento accionables desde una CLI.
Escuchar artículo

Terminas un entrenamiento, abres Strava y ves lo de siempre: distancia, ritmo medio, desnivel, pulsaciones y un mapa bonito. El problema no es que falten datos. El problema es que Strava te devuelve métricas, pero no interpretación operativa. Si quieres responder preguntas como “¿estoy acumulando fatiga?”, “¿mi pulso está demasiado alto para este ritmo?” o “¿qué patrón se repite en mis tiradas largas?”, tienes que hacer tú el trabajo mental.

Y si has intentado automatizar eso, ya sabes dónde empieza el dolor: OAuth, endpoints distintos, datos inconsistentes entre actividades, campos opcionales, streams enormes y un contexto que un modelo de lenguaje puede malinterpretar si le pasas ruido de más.

Si quieres una base más metodológica antes de llegar al código, te recomiendo este enfoque de Spec-Driven Development con OpenSpec en proyectos reales.

Por eso construí Running Agent alrededor de una idea muy simple: una CLI que extrae datos limpios de Strava, los normaliza, los valida y los deja listos para que un LLM pueda razonar sobre ellos. No es una app para reemplazar a Strava. Es una capa de análisis programable pensada para desarrolladores que quieren trabajar con sus propios datos.

El problema no es sacar datos, es convertirlos en contexto útil

Consumir una API no suele ser la parte difícil. Lo difícil es decidir qué información merece llegar al modelo y en qué formato. Si le mandas la actividad completa tal y como la devuelve Strava, mezclas campos irrelevantes, estructuras anidadas enormes y valores que no siempre están presentes.

Eso tiene tres efectos bastante malos:

  • El prompt se llena de ruido.
  • El coste sube porque mandas demasiado contexto.
  • El modelo empieza a inventar patrones a partir de datos mal preparados.

La clave está en construir una tubería intermedia:

  1. Autenticarte contra Strava.
  2. Extraer actividades o una actividad concreta.
  3. Normalizar nombres, unidades y campos opcionales.
  4. Validar con un esquema antes de generar contexto.
  5. Pasar al LLM solo lo que ayuda a responder una pregunta concreta.

Si no haces esto, no estás montando un asistente de entrenamiento. Estás pegando JSON al azar en una ventana de chat.

La arquitectura mínima que sí funciona

Para este tipo de herramienta preferí una arquitectura simple y aburrida antes que una app web más vistosa.

El flujo es este:

Strava OAuth -> Cliente API -> Normalización -> Validación Zod -> JSON estable -> Prompt -> LLM -> Informe

No tiene misterio, y precisamente por eso escala bien.

1. OAuth y extracción

La CLI se encarga del flujo de autenticación y del refresco de tokens. Quería que la parte incómoda ocurriera una vez y después trabajar siempre con comandos reproducibles.

Un ejemplo de uso real:

# Listar las últimas 5 actividades en JSON
npm run dev -- activities list --per-page 5 --json > activities.json

# Obtener el detalle de una actividad concreta
npm run dev -- activities details 123456789 --json > activity.json

La decisión de usar una CLI aquí no fue casual. Una web app queda mejor en una demo, pero una CLI tiene ventajas muy concretas:

  • Encaja mejor en flujos automatizados.
  • Produce JSON de salida fácil de encadenar.
  • Es más fácil de versionar y testear.
  • No te obliga a diseñar interfaz antes de entender el dominio.

Para mí, esa última razón pesa mucho. Cuando todavía estás explorando qué preguntas quieres responder, una terminal te deja avanzar más rápido que una UI.

2. Normalización

La API de Strava es amplia y útil, pero no está pensada para servir como contexto directo para un LLM. Hay campos que cambian según el tipo de deporte, campos opcionales y estructuras que solo tienen sentido para una capa muy concreta del producto.

Lo que hago es convertir la respuesta bruta a un objeto más estable:

{
  "id": 123456789,
  "name": "Tirada larga domingo",
  "sport_type": "Run",
  "distance_km": 18.52,
  "moving_time_min": 90,
  "elevation_gain_m": 210,
  "average_pace_min_km": 4.86,
  "average_heartrate": 155,
  "max_heartrate": 171,
  "suffer_score": 82,
  "start_date_local": "2026-04-13T08:00:00Z"
}

Aquí ya se ve una decisión importante: JSON como contrato intermedio. No quiero que la capa que habla con Strava y la capa que habla con el LLM dependan una de la otra. Entre ambas hay un formato estable que puedo inspeccionar, guardar, testear y reutilizar.

Diseñando la CLI para que sirva a humanos y a máquinas

Una CLI para análisis no debería limitarse a “mostrar cosas por pantalla”. Tiene que ofrecer salidas pensadas para distintos usos:

  • lectura humana en terminal,
  • integración con scripts,
  • exportación a JSON para agentes o prompts,
  • y, si hace falta, agregación de varias actividades.

Eso me llevó a separar bien los comandos y sus flags:

# Últimas actividades para inspección rápida
strava-cli activities list --per-page 10

# Salida estructurada para otra herramienta
strava-cli activities list --per-page 10 --json

# Detalle de una actividad para análisis profundo
strava-cli activities details 123456789 --json

El patrón aquí es simple: si el usuario pide --json, la salida deja de ser “bonita” y pasa a ser un contrato consumible. Eso parece obvio, pero muchas CLIs fallan justo ahí y mezclan formato visual con salida de integración.

Zod como filtro antes del análisis

Enviar datos sin validar a un LLM es una receta perfecta para análisis poco fiables. Si el modelo recibe un campo que a veces existe y a veces no, o una unidad inconsistente, el problema no es “que la IA falle”. El problema es que tú le has dado una base dudosa.

Por eso metí Zod en mitad del flujo.

import { z } from 'zod';

const ActivityDetailsSchema = z.object({
  id: z.number(),
  name: z.string(),
  sport_type: z.string(),
  distance_km: z.number(),
  moving_time_min: z.number(),
  elevation_gain_m: z.number(),
  average_pace_min_km: z.number(),
  average_heartrate: z.number().optional(),
  max_heartrate: z.number().optional(),
  suffer_score: z.number().optional(),
  start_date_local: z.string(),
});

type ActivityDetails = z.infer<typeof ActivityDetailsSchema>;

export function normalizeActivity(raw: unknown): ActivityDetails {
  return ActivityDetailsSchema.parse(raw);
}

Lo interesante no es solo evitar errores de runtime. Lo importante es que la validación obliga a decidir:

  • qué campos son obligatorios,
  • cuáles son opcionales,
  • qué nombres de propiedad quieres estabilizar,
  • y qué datos no vas a pasar al modelo.

Ese descarte es casi tan importante como la extracción.

Cómo preparar contexto para un LLM sin meter ruido

Aquí está la parte que más valor aporta y donde más fácil es equivocarse.

Un prompt pobre suele sonar así:

Analiza esta actividad de Strava y dime si estoy entrenando bien.

Eso es demasiado abierto. “Entrenar bien” no significa nada operativo. No acota el objetivo, no fija el tipo de salida y tampoco le dice al modelo cómo tratar incertidumbre o datos incompletos.

Una versión mucho mejor sería esta:

Actúa como analista de entrenamiento de resistencia.

Te paso una actividad y un resumen de las últimas 7 sesiones.
Quiero que detectes:
1. signos de fatiga acumulada,
2. desajuste entre ritmo y frecuencia cardiaca,
3. si la sesión encaja con una semana orientada a base aeróbica.

Responde en este formato:
- observaciones
- riesgos
- preguntas abiertas por falta de datos
- recomendación para la próxima sesión

Si falta contexto para sacar una conclusión sólida, dilo explícitamente.

La diferencia entre ambos prompts no está en sonar más “pro”. Está en que el segundo:

  • define un rol útil,
  • pide tareas concretas,
  • fija una estructura de salida,
  • y permite declarar incertidumbre.

Eso reduce bastante las respuestas bonitas pero vacías.

Un ejemplo de respuesta útil y cómo validarla

Supongamos que el modelo devuelve algo así:

La actividad muestra un pulso medio superior al habitual para ese rango de ritmo y una caída de cadencia en los últimos kilómetros. Eso sugiere fatiga acumulada. Reduciría intensidad 24-48 horas y priorizaría una sesión suave.

Suena razonable. Pero no deberías quedarte solo con que “suena bien”. Yo intentaría validar al menos tres cosas:

  1. Trazabilidad. ¿La respuesta hace referencia a datos que realmente están presentes?
  2. Moderación en la conclusión. ¿Está afirmando demasiado con poca evidencia?
  3. Accionabilidad. ¿La recomendación se puede convertir en una decisión concreta?

Si el modelo habla de cadencia y tú no le pasaste datos de cadencia, la respuesta queda invalidada. Si recomienda descanso absoluto por una sola actividad aislada, probablemente está sobrerreaccionando. Y si la salida es un párrafo motivacional sin siguiente paso, no te sirve para nada.

Tradeoffs reales: privacidad, coste y alucinaciones

La parte bonita de este stack es que puedes hacer preguntas muy útiles sobre tus entrenamientos. La parte menos bonita es que no deja de ser un sistema probabilístico apoyado en datos incompletos.

Estos son los límites más importantes:

Privacidad

Los datos de salud y rendimiento no son un dataset cualquiera. Si usas una API remota, estás mandando información sensible a un tercero. Por eso me gusta que la arquitectura sea agnóstica al modelo y permita usar alternativas locales si te importa más privacidad que comodidad.

Coste

Si mandas históricos largos o streams muy detallados, el coste del análisis crece rápido. De ahí la obsesión por filtrar y resumir antes de generar el prompt.

Alucinaciones

Un LLM puede detectar patrones útiles, pero también puede verbalizar con muchísima seguridad algo que no está respaldado por tus datos. Cuanto más ambiguo sea el contexto que le das, más espacio dejas para eso.

Qué no resuelve esta herramienta

Esto me parece importante decirlo porque si no el artículo suena a promesa mágica.

La herramienta no resuelve:

  • diagnóstico médico,
  • planificación deportiva profesional,
  • inferencias fiables si tus datos están incompletos,
  • ni comparaciones serias si mezclas deportes o dispositivos con métricas distintas.

Tampoco corrige malos datos de origen. Si el pulsómetro falla o la actividad está mal clasificada, el análisis sale torcido desde el principio. La IA no arregla una telemetría defectuosa.

Qué haría yo si empezara hoy

Si volviera a arrancar este proyecto desde cero, haría exactamente esto:

  1. Definiría 3 preguntas de análisis muy concretas antes de tocar prompts.
  2. Diseñaría el JSON intermedio primero y la UI después.
  3. Validaría con zod desde el primer día.
  4. Empezaría comparando una sola actividad con una ventana corta de histórico.
  5. Trataría cualquier recomendación del modelo como hipótesis, no como verdad.

Para mí, ahí está la gracia de combinar Strava + CLI + IA: no en tener “un entrenador automático”, sino en construir una herramienta que te permita explorar tus propios datos con más contexto y menos intuición ciega.

Si te interesa este enfoque, el siguiente paso no es montar una app enorme. Es algo mucho más pequeño: extrae 5 actividades, define un esquema estable y formula una sola pregunta que puedas validar con datos. Con eso ya tienes un sistema útil y bastante más serio que pegar JSON crudo en un chat.

Y cuando necesites presentar o depurar respuestas JSON complejas, este complemento te puede ahorrar tiempo: JSON Viewer interactivo para visualizar datos sin dolor.

Artículos relacionados

Newsletter

Aprende algo útil cada semana.

Ideas sobre programación, arquitectura e IA para mejorar tu código en menos de 5 minutos de lectura.

Sin spam · Baja cuando quieras