@josetra/dutic-mcp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -223
- package/dist/cli/index.js +89 -21
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ui.js +24 -0
- package/dist/cli/ui.js.map +1 -1
- package/dist/core/coursename.js +43 -0
- package/dist/core/coursename.js.map +1 -0
- package/dist/core/dates.js +44 -0
- package/dist/core/dates.js.map +1 -1
- package/dist/domain/fetch.js +67 -0
- package/dist/domain/fetch.js.map +1 -0
- package/dist/domain/people.js +166 -34
- package/dist/domain/people.js.map +1 -1
- package/dist/mcp/server.js +46 -17
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
- package/skills/dutic/SKILL.md +42 -6
package/README.md
CHANGED
|
@@ -1,223 +1,226 @@
|
|
|
1
|
-
# dutic-mcp
|
|
2
|
-
|
|
3
|
-
**Servidor MCP + CLI para el aula virtual DUTIC (Moodle) de la UNSA.**
|
|
4
|
-
Tus tareas —incluidas las **ocultas**—, notas, materiales de estudio y compañeros, en la terminal
|
|
5
|
-
y disponibles para agentes de IA (Claude Code, Antigravity, OpenCode, mimocode…).
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
┌─────────────────────────────────────────────┐
|
|
9
|
-
│ 24 tareas · 10 SIN ENTREGAR │
|
|
10
|
-
│ 18 ocultas que el calendario no te muestra │
|
|
11
|
-
└─────────────────────────────────────────────┘
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Por qué existe
|
|
15
|
-
|
|
16
|
-
El timeline de Moodle sólo muestra tareas **accionables** (futuras y sin entregar). Las tareas sin
|
|
17
|
-
fecha de calendario, ya vencidas o ya entregadas **desaparecen de la vista** — y así se pierden
|
|
18
|
-
entregas. `dutic` barre todos los cursos, encuentra esas tareas, las marca como `OCULTA` y ordena
|
|
19
|
-
lo pendiente por urgencia.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Instalación
|
|
24
|
-
|
|
25
|
-
**Requisitos:** [Node.js](https://nodejs.org) ≥ 20 y Google Chrome (o Edge) instalado.
|
|
26
|
-
No hace falta descargar Chromium: se usa el navegador que ya tienes.
|
|
27
|
-
|
|
28
|
-
### Un solo comando
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npm install -g @
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Esto deja `dutic` y `dutic-mcp` en tu PATH.
|
|
35
|
-
|
|
36
|
-
<details>
|
|
37
|
-
<summary>Instalar desde el código fuente</summary>
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
git clone https://github.com/JOSETRA44/dutic-mcp.git
|
|
41
|
-
cd dutic-mcp
|
|
42
|
-
npm install # compila automáticamente (script prepare)
|
|
43
|
-
npm install -g . # deja los comandos en el PATH
|
|
44
|
-
```
|
|
45
|
-
</details>
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
## Primeros pasos
|
|
50
|
-
|
|
51
|
-
### 1. Configura tus agentes (una vez)
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
dutic setup
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Registra el servidor MCP en los agentes que tengas instalados y copia la skill `dutic` a sus
|
|
58
|
-
directorios. Hace copia de seguridad (`*.dutic-bak`) y **no toca** el resto de tu configuración.
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
[OK] Claude Code (MCP) C:\Users\tu-usuario\.claude.json
|
|
62
|
-
[OK] Antigravity (MCP) ...\.antigravity\config\mcp_config.json
|
|
63
|
-
[OK] OpenCode (MCP) ...\.config\opencode\opencode.jsonc
|
|
64
|
-
[OK] mimocode (MCP) ...\.config\mimocode\mimocode.jsonc
|
|
65
|
-
[OK] Claude Code (skill) ...\.claude\skills\dutic
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
> Reinicia cada agente para que cargue el servidor.
|
|
69
|
-
|
|
70
|
-
### 2. Inicia sesión (una vez)
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
dutic login
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Se abre tu Chrome en el aula virtual. Pulsa **«Ingresar con Correo UNSA»**, elige tu cuenta de
|
|
77
|
-
Google y espera: la ventana se cierra sola al capturar la sesión. El perfil del navegador queda
|
|
78
|
-
guardado, así que las renovaciones posteriores suelen ser automáticas.
|
|
79
|
-
|
|
80
|
-
### 3. Compruébalo
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
dutic status # ¿sesión válida?
|
|
84
|
-
dutic tasks --all # tus tareas, incluidas las ocultas
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Si ves tus tareas, ya está todo listo. Pídeselo también a tu agente:
|
|
88
|
-
*«¿tengo alguna tarea pendiente en el aula virtual?»*
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## Uso — CLI
|
|
93
|
-
|
|
94
|
-
| Comando | Qué hace |
|
|
95
|
-
|---|---|
|
|
96
|
-
| `dutic tasks` | Tareas próximas del timeline (rápido) |
|
|
97
|
-
| `dutic tasks --all` | **+ barrido de cursos → incluye las ocultas** |
|
|
98
|
-
| `dutic tasks --hidden` | Sólo las ocultas |
|
|
99
|
-
| `dutic task <cmid>` | Detalle: consigna, fechas, adjuntos, conflicto de fechas |
|
|
100
|
-
| `dutic grades [id]` | Notas: resumen de todos los cursos, o detalle de uno |
|
|
101
|
-
| `dutic courses` | Cursos matriculados |
|
|
102
|
-
| `dutic materials <id> [--section "Tema 2"]` | Archivos del curso, por unidad |
|
|
103
|
-
| `dutic study <id> [--section "Tema 2"]` | Baja materiales y convierte PDFs a Markdown |
|
|
104
|
-
| `dutic read <url>` | Lee un recurso (PDF→Markdown) para analizarlo |
|
|
105
|
-
| `dutic md <archivo.pdf>` | Convierte un PDF local a Markdown |
|
|
106
|
-
| `dutic people <id
|
|
107
|
-
| `dutic person <texto>` | Busca
|
|
108
|
-
| `dutic
|
|
109
|
-
| `dutic
|
|
110
|
-
| `dutic
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
`
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
|
185
|
-
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
- La
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
1
|
+
# dutic-mcp
|
|
2
|
+
|
|
3
|
+
**Servidor MCP + CLI para el aula virtual DUTIC (Moodle) de la UNSA.**
|
|
4
|
+
Tus tareas —incluidas las **ocultas**—, notas, materiales de estudio y compañeros, en la terminal
|
|
5
|
+
y disponibles para agentes de IA (Claude Code, Antigravity, OpenCode, mimocode…).
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────┐
|
|
9
|
+
│ 24 tareas · 10 SIN ENTREGAR │
|
|
10
|
+
│ 18 ocultas que el calendario no te muestra │
|
|
11
|
+
└─────────────────────────────────────────────┘
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Por qué existe
|
|
15
|
+
|
|
16
|
+
El timeline de Moodle sólo muestra tareas **accionables** (futuras y sin entregar). Las tareas sin
|
|
17
|
+
fecha de calendario, ya vencidas o ya entregadas **desaparecen de la vista** — y así se pierden
|
|
18
|
+
entregas. `dutic` barre todos los cursos, encuentra esas tareas, las marca como `OCULTA` y ordena
|
|
19
|
+
lo pendiente por urgencia.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Instalación
|
|
24
|
+
|
|
25
|
+
**Requisitos:** [Node.js](https://nodejs.org) ≥ 20 y Google Chrome (o Edge) instalado.
|
|
26
|
+
No hace falta descargar Chromium: se usa el navegador que ya tienes.
|
|
27
|
+
|
|
28
|
+
### Un solo comando
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g @josetra/dutic-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Esto deja `dutic` y `dutic-mcp` en tu PATH.
|
|
35
|
+
|
|
36
|
+
<details>
|
|
37
|
+
<summary>Instalar desde el código fuente</summary>
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/JOSETRA44/dutic-mcp.git
|
|
41
|
+
cd dutic-mcp
|
|
42
|
+
npm install # compila automáticamente (script prepare)
|
|
43
|
+
npm install -g . # deja los comandos en el PATH
|
|
44
|
+
```
|
|
45
|
+
</details>
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Primeros pasos
|
|
50
|
+
|
|
51
|
+
### 1. Configura tus agentes (una vez)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
dutic setup
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Registra el servidor MCP en los agentes que tengas instalados y copia la skill `dutic` a sus
|
|
58
|
+
directorios. Hace copia de seguridad (`*.dutic-bak`) y **no toca** el resto de tu configuración.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
[OK] Claude Code (MCP) C:\Users\tu-usuario\.claude.json
|
|
62
|
+
[OK] Antigravity (MCP) ...\.antigravity\config\mcp_config.json
|
|
63
|
+
[OK] OpenCode (MCP) ...\.config\opencode\opencode.jsonc
|
|
64
|
+
[OK] mimocode (MCP) ...\.config\mimocode\mimocode.jsonc
|
|
65
|
+
[OK] Claude Code (skill) ...\.claude\skills\dutic
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> Reinicia cada agente para que cargue el servidor.
|
|
69
|
+
|
|
70
|
+
### 2. Inicia sesión (una vez)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
dutic login
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Se abre tu Chrome en el aula virtual. Pulsa **«Ingresar con Correo UNSA»**, elige tu cuenta de
|
|
77
|
+
Google y espera: la ventana se cierra sola al capturar la sesión. El perfil del navegador queda
|
|
78
|
+
guardado, así que las renovaciones posteriores suelen ser automáticas.
|
|
79
|
+
|
|
80
|
+
### 3. Compruébalo
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
dutic status # ¿sesión válida?
|
|
84
|
+
dutic tasks --all # tus tareas, incluidas las ocultas
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Si ves tus tareas, ya está todo listo. Pídeselo también a tu agente:
|
|
88
|
+
*«¿tengo alguna tarea pendiente en el aula virtual?»*
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Uso — CLI
|
|
93
|
+
|
|
94
|
+
| Comando | Qué hace |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `dutic tasks` | Tareas próximas del timeline (rápido) |
|
|
97
|
+
| `dutic tasks --all` | **+ barrido de cursos → incluye las ocultas** |
|
|
98
|
+
| `dutic tasks --hidden` | Sólo las ocultas |
|
|
99
|
+
| `dutic task <cmid>` | Detalle: consigna, fechas, adjuntos, conflicto de fechas |
|
|
100
|
+
| `dutic grades [id]` | Notas: resumen de todos los cursos, o detalle de uno |
|
|
101
|
+
| `dutic courses` | Cursos matriculados |
|
|
102
|
+
| `dutic materials <id> [--section "Tema 2"]` | Archivos del curso, por unidad |
|
|
103
|
+
| `dutic study <id> [--section "Tema 2"]` | Baja materiales y convierte PDFs a Markdown |
|
|
104
|
+
| `dutic read <url>` | Lee un recurso (PDF→Markdown) para analizarlo |
|
|
105
|
+
| `dutic md <archivo.pdf>` | Convierte un PDF local a Markdown |
|
|
106
|
+
| `dutic people <id>` | Todos los compañeros del curso, con correo (`--no-email` para omitir) |
|
|
107
|
+
| `dutic person <texto>` | Busca por nombre/correo: su correo y **sus cursos reales** (con grupo), marcando cuáles compartes |
|
|
108
|
+
| `dutic profile <userId>` | Perfil de cualquier id (docentes incluidos): correo y sus cursos |
|
|
109
|
+
| `dutic fetch <url>` | Explora cualquier página del aula por URL (cambiar ids, ver lo que no tiene botón) |
|
|
110
|
+
| `dutic teachers <id>` | Docentes del curso |
|
|
111
|
+
| `dutic pull <id>` | Descarga todos los materiales |
|
|
112
|
+
| `dutic setup` / `dutic login` / `dutic status` | Configuración y sesión |
|
|
113
|
+
|
|
114
|
+
Añade `--json` a la mayoría de comandos para salida estructurada.
|
|
115
|
+
|
|
116
|
+
### Ejemplos
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# ¿Qué me falta entregar?
|
|
120
|
+
dutic tasks --all
|
|
121
|
+
|
|
122
|
+
# Preparar sólo la unidad que voy a estudiar
|
|
123
|
+
dutic study 2279 --section "Tema 2" --dest ./materiales
|
|
124
|
+
|
|
125
|
+
# ¿Qué pide exactamente esta tarea?
|
|
126
|
+
dutic task 385686
|
|
127
|
+
|
|
128
|
+
# ¿Quién es y en qué cursos coincido con él? (correo + todos los cursos compartidos)
|
|
129
|
+
dutic person "Piero"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Uso — con agentes (MCP)
|
|
135
|
+
|
|
136
|
+
Tras `dutic setup` no hay nada más que hacer: pregúntale a tu agente por tus tareas, notas o
|
|
137
|
+
materiales y usará las herramientas del servidor.
|
|
138
|
+
|
|
139
|
+
<details>
|
|
140
|
+
<summary>Configuración manual (otros clientes MCP)</summary>
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"mcpServers": {
|
|
145
|
+
"dutic": {
|
|
146
|
+
"command": "dutic-mcp",
|
|
147
|
+
"env": { "DUTIC_SEMESTER": "2026A" }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Si tu cliente no resuelve comandos del PATH, usa la ruta absoluta que imprime `dutic setup`:
|
|
154
|
+
`{ "command": "node", "args": ["<ruta>/dist/mcp/server.js"] }`
|
|
155
|
+
</details>
|
|
156
|
+
|
|
157
|
+
**20 herramientas**: tareas (`dutic_list_tasks`, `dutic_get_assignment_detail`, …), notas
|
|
158
|
+
(`dutic_get_grades`), materiales (`dutic_list_course_materials`, `dutic_study_course`,
|
|
159
|
+
`dutic_read_resource`, `dutic_pdf_to_markdown`), personas (`dutic_list_participants`,
|
|
160
|
+
`dutic_find_person`, `dutic_get_person_profile`, `dutic_get_course_teachers`), exploración por URL
|
|
161
|
+
(`dutic_fetch_page`) y sesión.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Configuración
|
|
166
|
+
|
|
167
|
+
| Variable | Para qué | Por defecto |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `DUTIC_SEMESTER` | Semestre en la URL del aula (`2026A`, `2026B`…) | `2026A` |
|
|
170
|
+
| `DUTIC_BROWSER_CHANNEL` | Navegador para el login: `chrome`, `msedge`, `chromium` | `chrome` |
|
|
171
|
+
| `DUTIC_DATA_DIR` | Dónde guardar sesión y perfil | `~/.dutic` |
|
|
172
|
+
|
|
173
|
+
El semestre sólo se usa para la URL de login: tras iniciar sesión **se auto-detecta** del propio
|
|
174
|
+
aula, así que al cambiar de período normalmente no hay que tocar nada.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Cómo funciona
|
|
179
|
+
|
|
180
|
+
No usa la API pública de web services (la UNSA la tiene bloqueada). Captura la cookie
|
|
181
|
+
`MoodleSession` y el token `sesskey` tras el login de Google (Playwright manejando tu Chrome) y con
|
|
182
|
+
ellos llama al endpoint AJAX interno de Moodle, complementado con scraping donde hace falta.
|
|
183
|
+
|
|
184
|
+
| Necesidad | Fuente | Estado |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| Descubrir todas las tareas | `core_courseformat_get_state` | ✅ |
|
|
187
|
+
| Cursos matriculados | `core_course_get_enrolled_courses_by_timeline_classification` | ✅ |
|
|
188
|
+
| Timeline y fechas | `core_calendar_get_action_events_by_timesort` | ✅ (sólo accionables) |
|
|
189
|
+
| Estado de entrega, consigna, adjuntos | scraping de `mod/assign/view.php` | ✅ |
|
|
190
|
+
| Notas | scraping de `grade/report/user/index.php` | ✅ |
|
|
191
|
+
| Personas y correos | scraping de `user/index.php` y `user/view.php` | ✅ |
|
|
192
|
+
| `core_course_get_contents`, `mod_assign_*`, `gradereport_*` | — | ❌ bloqueadas por la UNSA |
|
|
193
|
+
|
|
194
|
+
**Fechas contradictorias:** algunas consignas mencionan una fecha distinta a la configurada en
|
|
195
|
+
Moodle. `dutic task <cmid>` compara ambas y avisa (`dateConflict`) — es la causa típica de entregas
|
|
196
|
+
perdidas.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Publicar en npm
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npm login # cuenta de npm
|
|
204
|
+
npm version patch # o minor / major
|
|
205
|
+
npm publish # el paquete es scoped y público (publishConfig.access)
|
|
206
|
+
git push --follow-tags
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`prepublishOnly` compila antes de publicar y `files` limita el tarball a `dist/` y `skills/`.
|
|
210
|
+
|
|
211
|
+
> Si tu scope de npm no es `@joswetra`, cambia el campo `name` en `package.json` por
|
|
212
|
+
> `@tu-scope/dutic-mcp` (o un nombre sin scope que esté libre).
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Privacidad y seguridad
|
|
217
|
+
|
|
218
|
+
- La sesión (`~/.dutic/session.json`) y el perfil del navegador contienen credenciales de tu
|
|
219
|
+
cuenta: no se versionan y el archivo se crea con permisos restrictivos.
|
|
220
|
+
- El certificado de `aulavirtual.unsa.edu.pe` (CA privada de la UNSA) se acepta **sólo** para ese host.
|
|
221
|
+
- La herramienta accede únicamente a lo que tú ya ves en el aula. Donde Moodle oculta información
|
|
222
|
+
(docentes en el listado, compañeros de otros grupos) se respeta esa restricción.
|
|
223
|
+
|
|
224
|
+
## Licencia
|
|
225
|
+
|
|
226
|
+
MIT © JOSETRA44
|
package/dist/cli/index.js
CHANGED
|
@@ -12,8 +12,11 @@ import { convertLocalPdfToMarkdown, readResourceAsMarkdown, studyCourseMaterials
|
|
|
12
12
|
import { getAllGrades, getCourseGrades } from "../domain/grades.js";
|
|
13
13
|
import { getAssignDetail } from "../domain/assign.js";
|
|
14
14
|
import { findPeople, getCourseTeachers, getPersonProfile, listCourseParticipants, } from "../domain/people.js";
|
|
15
|
+
import { fetchAulaPage } from "../domain/fetch.js";
|
|
16
|
+
import { parseCourseName } from "../core/coursename.js";
|
|
17
|
+
import { humanizeAgo } from "../core/dates.js";
|
|
15
18
|
import { formatTaskLine } from "./format.js";
|
|
16
|
-
import { banner, c, mark, progressBar, rule, table } from "./ui.js";
|
|
19
|
+
import { banner, c, mark, progressBar, rule, statusLine, table } from "./ui.js";
|
|
17
20
|
import { MCP_SERVER_PATH, runSetup } from "./setup.js";
|
|
18
21
|
import { readFileSync } from "node:fs";
|
|
19
22
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -158,20 +161,18 @@ program
|
|
|
158
161
|
});
|
|
159
162
|
program
|
|
160
163
|
.command("people <courseId>")
|
|
161
|
-
.description("Participantes
|
|
162
|
-
.option("--email", "
|
|
164
|
+
.description("Participantes del curso, con su correo (recorre todas las páginas).")
|
|
165
|
+
.option("--no-email", "No resolver los correos (más rápido).")
|
|
163
166
|
.option("--json", "Salida en JSON.")
|
|
164
167
|
.action(async (courseId, opts) => {
|
|
165
168
|
await withSession(async (session) => {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return [p.name, p.role ?? "—", p.group ?? "—", p.lastAccess ?? "—", ...(opts.email ? [email] : [])];
|
|
174
|
-
}));
|
|
169
|
+
const withEmail = opts.email !== false; // commander: --no-email ⇒ opts.email === false
|
|
170
|
+
const status = statusLine();
|
|
171
|
+
const ppl = await listCourseParticipants(session, Number(courseId), {
|
|
172
|
+
withEmail,
|
|
173
|
+
onProgress: ({ phase, done, total, label }) => status.set(`${phase} ${done}/${total} ${c.dim((label ?? "").slice(0, 34))}`),
|
|
174
|
+
});
|
|
175
|
+
status.done();
|
|
175
176
|
if (opts.json)
|
|
176
177
|
return out(JSON.stringify(ppl, null, 2));
|
|
177
178
|
out(banner("Participantes", `${ppl.length} · curso ${courseId}`));
|
|
@@ -180,8 +181,14 @@ program
|
|
|
180
181
|
{ header: "rol", color: c.dim },
|
|
181
182
|
{ header: "grupo", color: c.dim },
|
|
182
183
|
{ header: "último acceso", color: c.dim },
|
|
183
|
-
...(
|
|
184
|
-
],
|
|
184
|
+
...(withEmail ? [{ header: "correo", color: c.cyan }] : []),
|
|
185
|
+
], ppl.map((p) => [
|
|
186
|
+
p.name,
|
|
187
|
+
p.role ?? "—",
|
|
188
|
+
p.group ?? "—",
|
|
189
|
+
p.lastAccess ?? "—",
|
|
190
|
+
...(withEmail ? [p.email ?? "—"] : []),
|
|
191
|
+
])));
|
|
185
192
|
}, { login: { onStatus: log } });
|
|
186
193
|
});
|
|
187
194
|
program
|
|
@@ -190,16 +197,68 @@ program
|
|
|
190
197
|
.option("--json", "Salida en JSON.")
|
|
191
198
|
.action(async (query, opts) => {
|
|
192
199
|
await withSession(async (session) => {
|
|
193
|
-
const
|
|
200
|
+
const status = statusLine();
|
|
201
|
+
const found = await findPeople(session, query, {
|
|
202
|
+
onProgress: ({ phase, done, total, label }) => status.set(`${phase} ${done}/${total} ${c.dim((label ?? "").slice(0, 38))}`),
|
|
203
|
+
});
|
|
204
|
+
status.done();
|
|
194
205
|
if (opts.json)
|
|
195
206
|
return out(JSON.stringify(found, null, 2));
|
|
196
207
|
out(banner("Personas", `"${query}" · ${found.length} resultado(s)`));
|
|
197
208
|
for (const p of found) {
|
|
198
|
-
out(
|
|
199
|
-
out(` ${c.dim("
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
out(` ${c.dim("
|
|
209
|
+
out(`\n${mark.arrow()} ${c.bold(p.name)}`);
|
|
210
|
+
out(` ${c.dim("correo:")} ${p.email ? c.cyan(p.email) : c.gray("no visible")}`);
|
|
211
|
+
out(` ${c.dim("visto:")} ${p.lastAccess ?? c.gray("—")}` +
|
|
212
|
+
` ${c.dim(`(${humanizeAgo(p.lastSeenAgoSeconds)}, lo más reciente)`)}`);
|
|
213
|
+
out(` ${c.dim("cursos:")} ${c.bold(String(p.courses.length))} en total · ` +
|
|
214
|
+
`${c.green(String(p.sharedCount))} contigo`);
|
|
215
|
+
for (const cr of p.courses) {
|
|
216
|
+
const grp = cr.group ? c.dim(` · ${cr.group}`) : "";
|
|
217
|
+
const flag = cr.shared ? c.green("✓ contigo") : c.gray("· su curso");
|
|
218
|
+
const acc = cr.shared && cr.lastAccess ? c.dim(` visto ${cr.lastAccess}`) : "";
|
|
219
|
+
out(` ${cr.shared ? c.green("●") : c.gray("○")} ${cr.subject}${grp} ${flag}${acc}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}, { login: { onStatus: log } });
|
|
223
|
+
});
|
|
224
|
+
program
|
|
225
|
+
.command("profile <userId>")
|
|
226
|
+
.description("Perfil de cualquier usuario por id: correo y sus cursos (sirve para docentes).")
|
|
227
|
+
.option("--course <id>", "Curso de contexto que revela sus cursos (usa uno que compartas).")
|
|
228
|
+
.option("--json", "Salida en JSON.")
|
|
229
|
+
.action(async (userId, opts) => {
|
|
230
|
+
await withSession(async (session) => {
|
|
231
|
+
const prof = await getPersonProfile(session, Number(userId), opts.course ? Number(opts.course) : undefined);
|
|
232
|
+
if (opts.json)
|
|
233
|
+
return out(JSON.stringify(prof, null, 2));
|
|
234
|
+
out(banner("Perfil", prof.name));
|
|
235
|
+
out(` ${c.dim("id:")} ${prof.userId}`);
|
|
236
|
+
out(` ${c.dim("correo:")} ${prof.email ? c.cyan(prof.email) : c.gray("no visible")}`);
|
|
237
|
+
out(` ${c.dim("zona:")} ${prof.timezone ?? "—"}`);
|
|
238
|
+
out(` ${c.dim("cursos:")} ${c.bold(String(prof.courses.length))}`);
|
|
239
|
+
for (const cr of prof.courses) {
|
|
240
|
+
out(` ${mark.bullet()} ${cr.subject}${cr.group ? c.dim(` · ${cr.group}`) : ""} ${c.gray(`(id ${cr.courseId})`)}`);
|
|
241
|
+
}
|
|
242
|
+
if (!prof.courses.length) {
|
|
243
|
+
log(c.dim(" (sin cursos visibles; prueba --course <id de un curso que compartas>)"));
|
|
244
|
+
}
|
|
245
|
+
}, { login: { onStatus: log } });
|
|
246
|
+
});
|
|
247
|
+
program
|
|
248
|
+
.command("fetch <url>")
|
|
249
|
+
.description("Descarga cualquier página del aula con tu sesión (explorar por URL, cambiar ids…).")
|
|
250
|
+
.option("--format <f>", "text | html | links", "text")
|
|
251
|
+
.option("--max <n>", "Máximo de caracteres.", "20000")
|
|
252
|
+
.action(async (url, opts) => {
|
|
253
|
+
await withSession(async (session) => {
|
|
254
|
+
const r = await fetchAulaPage(session, url, opts.format, Number(opts.max));
|
|
255
|
+
log(c.dim(`# ${r.finalUrl} (${r.status})`));
|
|
256
|
+
if (opts.format === "links" && r.links) {
|
|
257
|
+
for (const l of r.links)
|
|
258
|
+
out(`${c.cyan(l.href)} ${c.dim(l.text)}`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
out(r.content);
|
|
203
262
|
}
|
|
204
263
|
}, { login: { onStatus: log } });
|
|
205
264
|
});
|
|
@@ -249,8 +308,17 @@ program
|
|
|
249
308
|
out(table([
|
|
250
309
|
{ header: "id", align: "right", color: c.dim },
|
|
251
310
|
{ header: "curso" },
|
|
311
|
+
{ header: "grupo", color: c.dim },
|
|
252
312
|
{ header: "docente(s)", color: c.dim },
|
|
253
|
-
], courses.map((cr) =>
|
|
313
|
+
], courses.map((cr) => {
|
|
314
|
+
const parsed = parseCourseName(cr.fullname);
|
|
315
|
+
return [
|
|
316
|
+
String(cr.id),
|
|
317
|
+
parsed.subject,
|
|
318
|
+
parsed.group ?? "—",
|
|
319
|
+
cr.contacts.join(", ") || "—",
|
|
320
|
+
];
|
|
321
|
+
})));
|
|
254
322
|
}, { login: { onStatus: log } });
|
|
255
323
|
});
|
|
256
324
|
const course = program.command("course").description("Operaciones sobre un curso.");
|