gestionar-cursos
Herramienta autónoma para gestión de cursos universitarios en la plataforma Moodle de Uniremington (aulavirtual.uniremington.edu.co).
Autenticación
Antes de cualquier navegación:
- REVISAR SESIÓN PERSISTENTE antes de pedir login:
.browserdata/(perfil Chrome con cookies de la sesión del Aula Virtual) existe en la raíz del workspace? → la sesión puede persistir; reusar el perfil (vianavegador_cdp) o leer cookies exportadas..moodle_session.jsonválido (expires_at> ahora, TTL 7 días)? → cargar cookies arequests.Session(vianavegador_requests), sin navegador.- Solo si NO hay sesión válida → ir al paso 1. No pedir login cuando ya
existe persistencia (el usuario inició sesión manualmente en el Aula
Virtual y las cookies quedaron en
.browserdata/).
- Navegar a
https://aulavirtual.uniremington.edu.co/my/ - Verificar estado:
- NO AUTENTICADO: URL contiene
login/index.phpo texto "Usted no se ha identificado" → Detener y pedir al usuario: "Por favor inicia sesión en Moodle manualmente" - AUTENTICADO: Ver nombre "Andres Felipe Rendon Hernandez" o encabezado "Área personal" → Continuar
- NO AUTENTICADO: URL contiene
Detección de Plataforma
El skill detecta automáticamente qué herramienta de navegación está disponible:
- browser_tool → navegación integrada (sin interfaz gráfica)
- open_browser → ventana externa del SO (navegador del sistema)
- selenium → Terminal / CLI (Chrome DevTools Protocol)
- Ninguna → Error con instrucciones de instalación
Terminal / CLI — Alternativa
Cuando se ejecuta desde terminal (sin agente IDE):
- Se conecta a Chrome vía CDP (
localhost:9222) - Si Chrome no está abierto con
--remote-debugging-port=9222, se lanza automáticamente una instancia visible - El usuario inicia sesión en Moodle en esa ventana
- El script espera y continúa la extracción automáticamente
Requisitos:
pip install selenium beautifulsoup4 lxml requests
Uso desde terminal:
cd gestionar-cursos/scripts
uv run python cli_init.py "https://aulavirtual.uniremington.edu.co/course/view.php?id=10272" \
--destino "C:/Users/.../Universidad/2026-2-B1"
El período y bloque se infieren automáticamente del --destino si la ruta
contiene el patrón YYYY-N-BX (ej: 2026-2-B1). También se pueden pasar
explícitamente con --periodo 2026-2 --bloque B1.
Integración con ClickUp
El skill extrae y organiza los archivos locales. ClickUp es el sistema
de registro para el seguimiento de completación de cada curso. El
skill nunca importa código de use-clickup — la integración ocurre
en el agente, que orquesta ambos skills.
clickup.json: índice de space/folder/lists/tareas por período.- Tags canónicos: ver
references/clickup-integracion.md(tags de tipo, evaluación, soporte + prioridades). - Flujo:
inityestadopreguntan al usuario si sincronizar;cli_clickup.py --dry-runpermite previsualizar antes de confirmar. - Aplicacion del
sync_plan.json: el agente sigue el playbook ejecutable en references/sync-flow.md.
Modo de operacion: autonomo por defecto. El agente invoca los
scripts y aplica el sync_plan.json sin pedir confirmacion, salvo
en los casos documentados en references/sync-flow.md §B.
Configuración LLM
El skill usa modelos de lenguaje para formatear documentos y resumir videos.
La configuración se centraliza en openrouter.json:
default_model: modelo principal (por defecto:google/gemma-4-31b-it:free)fallback_model: modelo de respaldo si el principal fallaprofiles: perfiles por tarea (document_formatter,youtube_summarizer)
Cada perfil define: system_prompt, temperature, max_tokens, timeout,
chunking (división automática de textos largos) y model/fallback
(null = hereda del nivel raíz).
Variables de entorno en .env:
OPENROUTER_API_KEY: clave de API OpenRouter (requerida)OPENROUTER_MODEL: anula el modelo por defecto
Jerarquía de ejecución:
- Agente nativo (
builtins.llm_complete) → sin costo extra - OpenRouter API → requiere
OPENROUTER_API_KEY - Sin LLM disponible → texto sin procesar
Caché: resultados LLM se cachean en _cache/<sha256>.json dentro
de cada carpeta de curso. Re-ejecuciones no gastan créditos en textos ya procesados.
Verificación de créditos: openrouter.json permite configurar
credit_threshold y credit_check para abortar si el saldo es insuficiente.
Flujos de Trabajo
gestionar-cursos init <URL> [<URL2> ...]
Uso: Inicializar uno o varios cursos desde URL(s) de Moodle.
Pasos:
- Verificar sesión en Moodle
- Navegar a la URL del curso proporcionada
- Extraer estructura de la barra lateral (todas las secciones)
- Navegar a "Introducción" y extraer:
- Visión general del curso (texto para AGENTS.md)
- Tabla PGA (DO-FR-66) — normalizar fechas a ISO 8601
- Tabla de sesiones sincrónicas — validar enlaces Teams
- Documentos introductorios: Módulo, Microcurrículo, y cualquier otro (PDF, DOCX, XLSX, PPTX)
- Los documentos se descargan a
MATERIA/y su texto se envía al LLM para limpieza + extracción de metadatos - Foros: Avisos, Foro de Consultas, Foro de Presentación — extraer TODAS las discusiones de primer nivel iniciadas por el profesor (solo la publicación original, no respuestas)
- Por cada unidad en la barra lateral:
- Expandir menús desplegables
- Buscar actividades del PGA
- Extraer descripción completa, instrucciones, materiales
- Consolidar: PGA información + detalle de unidad = actividad completa
- Detectar enlaces YouTube en páginas y módulos
url, extraer subtítulos conyt-dlp, resumir con LLM
- Descargar materiales (PDFs, documentos de apoyo)
- Crear estructura de carpetas local
- Generar archivos: AGENTS.md, CONTEXT.md, PGA.md, SITEMAP.md
Salida: Carpeta [CÓDIGO]-nombre-en-kebab-case con toda la estructura.
Metadatos LLM: AGENTS.md incluye sección ## Metadatos del Curso con
objetivos, competencias, metodología, criterios de evaluación, unidades
temáticas y bibliografía extraídos automáticamente de los documentos
introductorios.
Procesamiento paralelo: Si se pasan múltiples URLs con --parallel,
cada curso se procesa en un subproceso independiente. El proceso padre
verifica la sesión una sola vez y lanza los subprocesos con --no-browser
para que compartan la misma instancia de Chrome CDP.
uv run python cli_init.py <url1> <url2> <url3> --parallel --destino .
Re-inicialización: Si el curso ya existe localmente, init detecta
AGENTS.md y redirige a sincronización selectiva:
- Refresca secciones marcadas
<!-- auto -->desde Moodle. - Preserva secciones marcadas
<!-- manual -->(ej: PERIOD, BLOCK editados a mano). - Documentos introductorios se vuelven a extraer y fusionan.
gestionar-cursos clickup-sync <PERIODO_DIR>
Uso: Sincronizar la estructura local de todo un período con ClickUp.
uv run python cli_clickup.py "C:/Users/.../Universidad/2026-2-B1"
uv run python cli_clickup.py "C:/Users/.../Universidad/2026-2-B1" --dry-run
Qué hace:
- Resuelve
folder.iden el espacio "Universidad" (crea folder si no existe) - Por cada curso en
clickup.jsonconlist_id: null, resuelve/crea la lista - Por cada actividad no sincronizada crea tarea con tags y prioridad, usando
start_dateydue_datesegún la fuente de verdad (ver Fuente de Verdad de Fechas más abajo):- Primario:
_cache/snapshot.json(fecha_apertura/fecha_cierre) extraídas porestadode las páginas reales de Moodle. - Secundario (fallback):
PGA.mdpara actividades no visitables (páginas, URLs) o si la snapshot no tiene fechas.
- Primario:
- Actualiza
AGENTS.mdconCLICKUP_LIST_IDyclickup.jsoncon los IDs resueltos y las fechas aplicadas (start_date/due_dateISO por tarea). - Si la tarea ya existe pero cambió la fecha (según snapshot), la actualiza automáticamente — nunca revierte una fecha real de Moodle por la del PGA.
⚠ No reviertas con el PGA: la sección Fuente de Verdad de Fechas explica por qué
PGA fecha_finno es autoritativa. Sicli_clickup.pyse ejecuta sin snapshot fresca, ejecutaestadoprimero.
gestionar-cursos estado <CARPETA>
Uso: Verificar cambios en el curso comparando contra la última fotografía
(_cache/snapshot.json). El agente ejecuta esto automáticamente si la
snapshot tiene más de 24h de antigüedad.
Flujo:
- Cargar
_cache/snapshot.json(creado porinit) - Extraer barra lateral actual de Moodle
- Comparar URLs → detectar actividades nuevas, eliminadas y existentes
- En paralelo: un subproceso por unidad visita cada
quiz/assign/forum/lesson/workshopy extrae fechas dediv[data-region='activity-dates'](Abrió/Cierra/Vencimiento) → ISO 8601 en hora Colombia (UTC-5) - Comparar fechas extraídas vs snapshot → detectar cambios de deadline
- Guardar nueva snapshot actualizada (autoritativa para ClickUp)
- Reportar diff
La snapshot es la fuente de verdad para fechas.
cli_clickup.pydebe preferirla sobrePGA.md(ver Fuente de Verdad de Fechas).
Salida: Reporte en conversación:
## 🆕 Actividades nuevas (2)
- Actividad X (quiz) — Unidad 3
- Foro Y (forum) — Unidad 2
## 📅 Fechas modificadas (1)
- Primer parcial (Unidad 1)
- Cierre: 2026-02-15 → **2026-02-22**
## 🗑️ Eliminadas/ocultas (1)
- ~~Actividad antigua~~ (Unidad 1)
Si se usa --sync, el agente puede re-ejecutar init para descargar el
contenido de las actividades nuevas. Las fechas modificadas se reflejan en
la snapshot automáticamente.
El agente también usa use-clickup para actualizar las tareas de ClickUp
si detecta cambios de fecha o nuevas actividades.
gestionar-cursos foros <CARPETA>
Extrae foros evaluables (>0% en titulo) y los hasta 20 hilos
principales por foro. Output: Unidad-X/Foros/<slug>.md. Cache por
discuss_id — re-ejecuciones no re-abren hilos ya guardados. Se
invoca durante init para cada foro evaluable; tambien se puede
correr manual:
uv run python cli_foros.py "C:/.../2026-2-B1/MATERIA"
uv run python cli_foros.py "C:/.../2026-2-B1/MATERIA" --dry-run
Detalle de selectores HTML, formato de cache, casos edge y el cap de 20 en references/foros-evaluables.md.
gestionar-cursos calificaciones <CARPETA>
Uso: Extraer calificaciones del gradebook del estudiante en un
curso Moodle. Aplica a todos los items evaluables del curso: cuestionarios,
lecciones, talleres (envío + evaluación), tareas, foros. Actualiza
_cache/calificaciones_<courseid>.json, agrega campo calificacion en
cada actividad del snapshot.json, e inyecta sección ## Calificación
en el archivo .md de cada actividad.
cd gestionar-cursos/scripts
uv run python cli_calificaciones.py "C:/.../2026-2-B1/2607B04G1-línea-de-énfasis-1"
uv run python cli_calificaciones.py "C:/.../2026-2-B1/2607B04G1-línea-de-énfasis-1" --dry-run
Cuándo correrlo:
- Después de
estado(que refresca fechas). - Tras un parcial o tarea calificada por el docente.
- Antes de la sesión sincrónica para ver el avance del curso.
Orden correcto del flujo de sincronización:
# 1) Refrescar fechas
uv run python cli_estado.py "C:/.../2026-2-B1/<curso>"
# 2) Capturar calificaciones (sobre snapshot recien refrescado)
uv run python cli_calificaciones.py "C:/.../2026-2-B1/<curso>"
# 3) Sincronizar tareas con ClickUp (crear/actualizar fechas, tags)
uv run python cli_clickup.py "C:/.../2026-2-B1"
# 4) Sincronizar calificaciones capturadas → ClickUp (status + comentario)
uv run python sync_calificaciones_clickup.py "C:/.../2026-2-B1/<curso>"
cli_estado.py preserva el campo calificacion y el
calificaciones_capturadas al re-escribir snapshot.json. Re-ejecutar
estado después de calificaciones no borra las notas. Orden
inverso = bug: si se ejecuta calificaciones antes que estado, las
calificaciones se preservan; pero si se ejecuta calificaciones y
luego estado sin preservar, las calificaciones se pierden. (Desde
2026-2-B1 está arreglado en guardar_snapshot.)
Lo que captura por actividad:
| Campo | Fuente Moodle |
|---|---|
nota |
Calificación en escala del rango |
estado |
Aprobado / Reprobado / Pendiente (icono fa-check text-success / fa-remove text-danger) |
rango |
Rango calificable (ej. 0–5) |
porcentaje |
% sobre el rango |
aporte_curso |
% ya ponderado al total del curso |
ponderacion_categoria |
Ponderación dentro de la categoría del curso |
feedback |
Retroalimentación del docente |
Match de archivos .md: por nombre normalizado + alias canónicos
(Prueba Inicial → PruebaInicial.md, Primer Parcial → Parcial-1.md,
etc.). Si una actividad del gradebook no tiene .md local (típico:
H5P Contenido interactivo), el script lo reporta como warning pero
no falla.
Cache: el HTML crudo se guarda en
_cache/gradebook_<courseid>.html para auditoría, y el JSON
estructurado en _cache/calificaciones_<courseid>.json. Re-ejecuciones
sobre-escriben — no hay merge acumulativo.
gestionar-cursos sync-clickup-calificaciones <CARPETA>
Uso: Para cada tarea calificada en el snapshot.json del curso,
marca la tarea de ClickUp como "calificado" (status closed) y deja
un comentario con el detalle de la nota. Es idempotente: re-ejecuciones
no duplican status ni comentarios.
cd gestionar-cursos/scripts
uv run python sync_calificaciones_clickup.py "C:/.../2026-2-B1/<curso>"
uv run python sync_calificaciones_clickup.py "C:/.../2026-2-B1/<curso>" --dry-run
Prerrequisito: ejecutar cli_calificaciones.py primero para poblar
snapshot.json:actividades[*].calificacion. Si no hay calificaciones
capturadas, el script no hace nada (no hay qué sincronizar).
Cuándo correrlo:
- Después de
cli_calificaciones.pycuando aparecen nuevas notas (parcial calificado, tarea devuelta, etc). - En el flujo de revisión de fin de semana para tener el tablero de ClickUp al día.
Qué hace por cada tarea con calificacion.nota:
- Cruza por nombre entre
snapshot.jsonyclickup.json(con matching exacto + flexible). Si no encuentratask_id, warning. - Verifica idempotencia:
tarea_ya_calificada: ¿status ya es "calificado"?tiene_comentario_sync: ¿hay un comentario con tag[calificaciones-auto]?- Si ambas → SKIP (no duplica status ni comentario).
- PUT /task/{id} con
{"status": "calificado"}(usa el NOMBRE, no el status_id — la API de ClickUp rechaza IDs con 400). POST /task/{id}/commentcon formato:[calificaciones-auto] Calificación sincronizada desde Moodle - **Curso:** LÍNEA DE ÉNFASIS 1 - 2607B04G1 - **Actividad:** Primer Parcial (25%) - **Nota:** 4,80 / 0–5 (96,00 %) - **Estado:** Aprobado - **Aporte al curso:** 24,00 % - **Capturado:** 2026-07-17T01:21:11
Status personalizado: el script busca el status "calificado" en el
space Universidad (id 901311224662). El status se creó manualmente
en el space como tipo closed. Si no existe, el script falla con
lista de statuses disponibles. Otros espacios no-Universidad necesitan
su propio status (o el script debe parametrizar el nombre).
Lección (2026-2-B1, LPA 1 + Línea de Énfasis 1): la API de
ClickUp PUT /task/{id} rechaza el status_id con 400 Bad Request
cuando se envía el id (p901311224662_MhIABMss). Hay que enviar el
nombre del status ("calificado"). El client.py no documenta
esto explícitamente; el bug se manifiesta como "el status se queda en
pendiente aunque la respuesta sea 200" o como 400 directo.
Política de Errores
Resiliente — nunca falla completamente.
| Escenario | Comportamiento |
|---|---|
| Moodle caído / tiempo de espera | Reintentar 3 veces (1s, 2s, 4s). Si persiste, error con mensaje. |
| Actividad no encontrada | Advertencia + marcar [DETALLE_NO_ENCONTRADO]. Continuar. |
| PDF bloqueado | Advertencia + guardar enlace en lugar de archivo. No detener. |
| H5P no carga | Omitir proxy + enlace original en el mapa del sitio. |
| Sesión expirada | Detectar redirección a inicio de sesión → pausar + pedir volver a iniciar sesión. |
| Cambio en estructura HTML | Guardar HTML sin procesar para depuración + advertencia. Procesar lo posible. |
| LLM no disponible | Usar texto extraído sin formatear. Continuar sin metadatos. |
| OpenRouter sin créditos | Abortar llamadas LLM si saldo < umbral configurado. |
Formato de Fechas
Ver references/fechas-fuente-de-verdad.md.
Fuente de Verdad de Fechas
Regla de oro: la fuente de verdad operativa de las fechas de entrega NO es el PGA, sino las fechas de apertura y cierre configuradas por el profesor en cada actividad de Moodle.
Flujo:
initextrae el PGA como referencia de planeación.estadoextrae las fechas reales de Moodle →_cache/snapshot.json.cli_clickup.pyprefieresnapshot.jsonsobrePGA.md.- Tras sincronizar, actualizar
PGA.mdpara que refleje las fechas reales.
Detalle, selectores HTML, y la lección dura del 2026-2-B1 en
references/fechas-fuente-de-verdad.md.
Estructura de Carpetas Local
Ver references/folder-structure.md.
AGENTS.md - Contenido
Ver references/agents-md-template.md.
Archivos del Skill
Puntos de Entrada (CLI)
script: cli_init.py — entry point principal que orquesta el pipeline completo importando 17 scripts: navegación (browser_api, navegador_cdp, navegador_requests), extracción por tipo de módulo (extractor_modulos, extractor_foro, extractor_documentos, extractor_youtube), scaffolding (scaffold_curso, parsear_pga, parsear_sesiones), procesamiento LLM (llm_api, openrouter_client, formatear_llm), y control de sesión (moodle_session, verificar_sesion, checkpoint).
| Archivo | Propósito |
|---|---|
cli_init.py |
Inicializar curso(s) desde URL(s) de Moodle |
cli_estado.py |
Verificar estado y sincronización |
cli_calificaciones.py |
Extraer calificaciones del gradebook e inyectar ## Calificación en .md |
sync_calificaciones_clickup.py |
Sincronizar calificaciones Moodle → ClickUp (status='calificado' + comentario) |
cli_clickup.py |
Sincronizar cursos locales con ClickUp (IDs, tareas, tags) |
Extracción de Moodle
| Archivo | Propósito |
|---|---|
navegador_cdp.py |
Navegador Chrome DevTools Protocol + Selenium |
navegador_requests.py |
Backend alternativo: requests + BS4 sin navegador real |
browser_api.py |
Capa de abstracción IDE ↔ CDP |
moodle_session.py |
Exportación de cookies Selenium → requests |
verificar_sesion.py |
Detección de sesión activa en Moodle |
extractor_modulos.py |
Extractores por tipo (page, quiz, forum, resource, folder, hvp, assign, url) |
extractor_foro.py |
Discusiones de foros del profesor (flujo intro/Avisos/Consultas) |
extractor_foro_evaluable.py |
Foros evaluables (>0%): metadata + hilos principales, cap 20, cache por discuss_id |
cli_foros.py |
CLI: gestionar-cursos foros <CARPETA> — renderiza foros evaluables a Unidad-X/Foros/ |
_procesar_foro_evaluable.py |
Wrapper usado por cli_init para procesar un foro evaluable dentro del loop por actividad |
extractor_documentos.py |
PDF/DOCX/XLSX/PPTX → texto |
extractor_youtube.py |
Subtítulos YouTube vía yt-dlp + resumen LLM |
parsear_pga.py |
Tabla DO-FR-66, fechas ISO 8601 |
parsear_sesiones.py |
Cronograma con enlaces reales Teams |
scaffold_curso.py |
Estructura de carpetas, AGENTS.md, CONTEXT.md, SITEMAP.md |
checkpoint.py |
Punto de control .progress.json para reanudación |
LLM
| Archivo | Propósito |
|---|---|
openrouter_client.py |
Cliente OpenRouter con caché, fragmentación, reintentos, verificación de créditos |
llm_api.py |
Abstracción agente nativo → OpenRouter como respaldo |
formatear_llm.py |
Formateo de documentos + extracción de metadatos JSON |
openrouter.json |
Configuración centralizada: modelos, instrucciones, umbrales (raíz del skill) |
Utilidades
| Archivo | Propósito |
|---|---|
sincronizar_curso.py |
Detección de cambios Moodle contra local |
verificar_integridad.py |
Validación de archivos locales |
_extraer_fechas_unidad.py |
Subproceso: extrae fechas de quiz/assign por unidad |
detectar_plataforma.py |
Auto-detección de herramienta de navegación |
crear_proxy_h5p.py |
Generador de HTML proxy para contenido H5P |
descargar_materiales.py |
Descarga con forcedownload |
extraer_unidad.py |
Extracción a nivel de unidad |
verify.py |
Verificación de integridad del espacio de trabajo |
debug_profesor.py |
Utilidad de depuración para detección de profesor |