@josetra/dutic-mcp 0.1.0

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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/dist/cli/format.js +69 -0
  4. package/dist/cli/format.js.map +1 -0
  5. package/dist/cli/index.js +426 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/cli/setup.js +121 -0
  8. package/dist/cli/setup.js.map +1 -0
  9. package/dist/cli/ui.js +111 -0
  10. package/dist/cli/ui.js.map +1 -0
  11. package/dist/core/auth.js +47 -0
  12. package/dist/core/auth.js.map +1 -0
  13. package/dist/core/browser.js +56 -0
  14. package/dist/core/browser.js.map +1 -0
  15. package/dist/core/config.js +34 -0
  16. package/dist/core/config.js.map +1 -0
  17. package/dist/core/dates.js +43 -0
  18. package/dist/core/dates.js.map +1 -0
  19. package/dist/core/errors.js +25 -0
  20. package/dist/core/errors.js.map +1 -0
  21. package/dist/core/http.js +43 -0
  22. package/dist/core/http.js.map +1 -0
  23. package/dist/core/login.js +193 -0
  24. package/dist/core/login.js.map +1 -0
  25. package/dist/core/models.js +79 -0
  26. package/dist/core/models.js.map +1 -0
  27. package/dist/core/moodleClient.js +94 -0
  28. package/dist/core/moodleClient.js.map +1 -0
  29. package/dist/core/session.js +64 -0
  30. package/dist/core/session.js.map +1 -0
  31. package/dist/domain/assign.js +112 -0
  32. package/dist/domain/assign.js.map +1 -0
  33. package/dist/domain/concurrency.js +17 -0
  34. package/dist/domain/concurrency.js.map +1 -0
  35. package/dist/domain/courses.js +70 -0
  36. package/dist/domain/courses.js.map +1 -0
  37. package/dist/domain/documents.js +209 -0
  38. package/dist/domain/documents.js.map +1 -0
  39. package/dist/domain/grades.js +99 -0
  40. package/dist/domain/grades.js.map +1 -0
  41. package/dist/domain/people.js +161 -0
  42. package/dist/domain/people.js.map +1 -0
  43. package/dist/domain/resources.js +242 -0
  44. package/dist/domain/resources.js.map +1 -0
  45. package/dist/domain/tasks.js +191 -0
  46. package/dist/domain/tasks.js.map +1 -0
  47. package/dist/mcp/server.js +261 -0
  48. package/dist/mcp/server.js.map +1 -0
  49. package/package.json +67 -0
  50. package/skills/dutic/SKILL.md +173 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JOSETRA44
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,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 @joswetra/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> [--email]` | Compañeros del curso (con correo) |
107
+ | `dutic person <texto>` | Busca a alguien por nombre o correo |
108
+ | `dutic teachers <id>` | Docentes del curso |
109
+ | `dutic pull <id>` | Descarga todos los materiales |
110
+ | `dutic setup` / `dutic login` / `dutic status` | Configuración y sesión |
111
+
112
+ Añade `--json` a la mayoría de comandos para salida estructurada.
113
+
114
+ ### Ejemplos
115
+
116
+ ```bash
117
+ # ¿Qué me falta entregar?
118
+ dutic tasks --all
119
+
120
+ # Preparar sólo la unidad que voy a estudiar
121
+ dutic study 2279 --section "Tema 2" --dest ./materiales
122
+
123
+ # ¿Qué pide exactamente esta tarea?
124
+ dutic task 385686
125
+
126
+ # El correo de mi compañero de grupo
127
+ dutic person "Piero"
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Uso — con agentes (MCP)
133
+
134
+ Tras `dutic setup` no hay nada más que hacer: pregúntale a tu agente por tus tareas, notas o
135
+ materiales y usará las herramientas del servidor.
136
+
137
+ <details>
138
+ <summary>Configuración manual (otros clientes MCP)</summary>
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "dutic": {
144
+ "command": "dutic-mcp",
145
+ "env": { "DUTIC_SEMESTER": "2026A" }
146
+ }
147
+ }
148
+ }
149
+ ```
150
+
151
+ Si tu cliente no resuelve comandos del PATH, usa la ruta absoluta que imprime `dutic setup`:
152
+ `{ "command": "node", "args": ["<ruta>/dist/mcp/server.js"] }`
153
+ </details>
154
+
155
+ **19 herramientas**: tareas (`dutic_list_tasks`, `dutic_get_assignment_detail`, …), notas
156
+ (`dutic_get_grades`), materiales (`dutic_list_course_materials`, `dutic_study_course`,
157
+ `dutic_read_resource`, `dutic_pdf_to_markdown`), personas (`dutic_list_participants`,
158
+ `dutic_find_person`, `dutic_get_course_teachers`) y sesión.
159
+
160
+ ---
161
+
162
+ ## Configuración
163
+
164
+ | Variable | Para qué | Por defecto |
165
+ |---|---|---|
166
+ | `DUTIC_SEMESTER` | Semestre en la URL del aula (`2026A`, `2026B`…) | `2026A` |
167
+ | `DUTIC_BROWSER_CHANNEL` | Navegador para el login: `chrome`, `msedge`, `chromium` | `chrome` |
168
+ | `DUTIC_DATA_DIR` | Dónde guardar sesión y perfil | `~/.dutic` |
169
+
170
+ El semestre sólo se usa para la URL de login: tras iniciar sesión **se auto-detecta** del propio
171
+ aula, así que al cambiar de período normalmente no hay que tocar nada.
172
+
173
+ ---
174
+
175
+ ## Cómo funciona
176
+
177
+ No usa la API pública de web services (la UNSA la tiene bloqueada). Captura la cookie
178
+ `MoodleSession` y el token `sesskey` tras el login de Google (Playwright manejando tu Chrome) y con
179
+ ellos llama al endpoint AJAX interno de Moodle, complementado con scraping donde hace falta.
180
+
181
+ | Necesidad | Fuente | Estado |
182
+ |---|---|---|
183
+ | Descubrir todas las tareas | `core_courseformat_get_state` | ✅ |
184
+ | Cursos matriculados | `core_course_get_enrolled_courses_by_timeline_classification` | ✅ |
185
+ | Timeline y fechas | `core_calendar_get_action_events_by_timesort` | ✅ (sólo accionables) |
186
+ | Estado de entrega, consigna, adjuntos | scraping de `mod/assign/view.php` | ✅ |
187
+ | Notas | scraping de `grade/report/user/index.php` | ✅ |
188
+ | Personas y correos | scraping de `user/index.php` y `user/view.php` | ✅ |
189
+ | `core_course_get_contents`, `mod_assign_*`, `gradereport_*` | — | ❌ bloqueadas por la UNSA |
190
+
191
+ **Fechas contradictorias:** algunas consignas mencionan una fecha distinta a la configurada en
192
+ Moodle. `dutic task <cmid>` compara ambas y avisa (`dateConflict`) — es la causa típica de entregas
193
+ perdidas.
194
+
195
+ ---
196
+
197
+ ## Publicar en npm
198
+
199
+ ```bash
200
+ npm login # cuenta de npm
201
+ npm version patch # o minor / major
202
+ npm publish # el paquete es scoped y público (publishConfig.access)
203
+ git push --follow-tags
204
+ ```
205
+
206
+ `prepublishOnly` compila antes de publicar y `files` limita el tarball a `dist/` y `skills/`.
207
+
208
+ > Si tu scope de npm no es `@joswetra`, cambia el campo `name` en `package.json` por
209
+ > `@tu-scope/dutic-mcp` (o un nombre sin scope que esté libre).
210
+
211
+ ---
212
+
213
+ ## Privacidad y seguridad
214
+
215
+ - La sesión (`~/.dutic/session.json`) y el perfil del navegador contienen credenciales de tu
216
+ cuenta: no se versionan y el archivo se crea con permisos restrictivos.
217
+ - El certificado de `aulavirtual.unsa.edu.pe` (CA privada de la UNSA) se acepta **sólo** para ese host.
218
+ - La herramienta accede únicamente a lo que tú ya ves en el aula. Donde Moodle oculta información
219
+ (docentes en el listado, compañeros de otros grupos) se respeta esa restricción.
220
+
221
+ ## Licencia
222
+
223
+ MIT © JOSETRA44
@@ -0,0 +1,69 @@
1
+ import { c } from "./ui.js";
2
+ export function formatDate(epochSeconds) {
3
+ if (epochSeconds == null)
4
+ return "sin fecha";
5
+ const d = new Date(epochSeconds * 1000);
6
+ return d.toLocaleString("es-PE", {
7
+ day: "2-digit",
8
+ month: "2-digit",
9
+ year: "numeric",
10
+ hour: "2-digit",
11
+ minute: "2-digit",
12
+ });
13
+ }
14
+ export function relativeDue(epochSeconds) {
15
+ if (epochSeconds == null)
16
+ return "";
17
+ const diffMs = epochSeconds * 1000 - Date.now();
18
+ const days = Math.round(diffMs / 86_400_000);
19
+ if (days < 0)
20
+ return `vencida hace ${Math.abs(days)}d`;
21
+ if (days === 0)
22
+ return "¡hoy!";
23
+ if (days === 1)
24
+ return "mañana";
25
+ return `en ${days}d`;
26
+ }
27
+ const SUBMISSION = {
28
+ submitted: c.green,
29
+ "not-submitted": c.boldRed,
30
+ graded: c.cyan,
31
+ unknown: c.gray,
32
+ };
33
+ const SUBMISSION_LABEL = {
34
+ submitted: "ENTREGADA",
35
+ "not-submitted": "SIN ENTREGAR",
36
+ graded: "CALIFICADA",
37
+ unknown: "—",
38
+ };
39
+ /**
40
+ * Renderiza una tarea como bloque de líneas alineadas. Marca de estado a la izquierda:
41
+ * ● (pendiente, rojo) resalta lo urgente sin emojis.
42
+ */
43
+ export function formatTaskLine(t) {
44
+ const paint = SUBMISSION[t.submission] ?? c.gray;
45
+ const dot = t.submission === "not-submitted" ? c.boldRed("●") : c.gray("○");
46
+ const scope = t.hidden ? c.yellow("OCULTA") : c.dim("timeline");
47
+ const state = paint(SUBMISSION_LABEL[t.submission] ?? "—");
48
+ const grade = t.grade ? c.dim(` · nota ${t.grade}`) : "";
49
+ const due = `${formatDate(t.dueDate)}${t.dueDate != null ? c.dim(` (${relativeDue(t.dueDate)})`) : ""}`;
50
+ const lines = [
51
+ `${dot} ${c.bold(t.name)} ${c.gray("[")}${scope}${c.gray("]")}`,
52
+ ` ${c.dim("curso:")} ${t.courseName}`,
53
+ ` ${c.dim("entrega:")} ${due}`,
54
+ ` ${c.dim("estado:")} ${state}${grade}`,
55
+ ];
56
+ if (t.timeRemaining)
57
+ lines.push(` ${c.dim("resta:")} ${t.timeRemaining}`);
58
+ if (t.dateConflict) {
59
+ const mencionadas = t.datesInDescription.map((d) => d.text).join(", ");
60
+ lines.push(` ${c.boldRed("[!] OJO:")} la consigna menciona ${c.yellow(mencionadas)} — distinta a la fecha oficial`);
61
+ }
62
+ if (t.attachments.length) {
63
+ lines.push(` ${c.dim("adjuntos:")} ${t.attachments.map((a) => a.filename).join(", ")}`);
64
+ }
65
+ if (t.url)
66
+ lines.push(` ${c.gray(t.url)}`);
67
+ return lines.join("\n");
68
+ }
69
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/cli/format.ts"],"names":[],"mappings":"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"}