utn-mcp 0.8.3 → 0.9.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.
Files changed (86) hide show
  1. package/README.md +30 -3
  2. package/build/aula-virtual/client/client.d.ts +97 -0
  3. package/build/aula-virtual/client/credentials.d.ts +10 -0
  4. package/build/aula-virtual/client/link.d.ts +21 -0
  5. package/build/aula-virtual/client/local-session.d.ts +23 -0
  6. package/build/aula-virtual/client/moodle-api.d.ts +25 -0
  7. package/build/aula-virtual/client/regionals.d.ts +47 -0
  8. package/build/aula-virtual/client/session.d.ts +12 -0
  9. package/build/aula-virtual/client/sso-login.d.ts +60 -0
  10. package/build/aula-virtual/client/stored-session.d.ts +32 -0
  11. package/build/aula-virtual/client/types.d.ts +173 -0
  12. package/build/aula-virtual/format/actions.d.ts +57 -0
  13. package/build/aula-virtual/format/activities.d.ts +86 -0
  14. package/build/aula-virtual/format/address.d.ts +36 -0
  15. package/build/aula-virtual/format/fields.d.ts +11 -0
  16. package/build/aula-virtual/format/files.d.ts +32 -0
  17. package/build/aula-virtual/format/navigate.d.ts +213 -0
  18. package/build/aula-virtual/format/study-pack.d.ts +33 -0
  19. package/build/aula-virtual/format/submission.d.ts +40 -0
  20. package/build/aula-virtual/login.d.ts +6 -0
  21. package/build/aula-virtual/tools.d.ts +4 -0
  22. package/build/autogestion/client/catalog.d.ts +20 -0
  23. package/build/autogestion/client/catalog.js +22 -0
  24. package/build/autogestion/client/connector.d.ts +71 -0
  25. package/build/autogestion/client/connector.js +31 -0
  26. package/build/autogestion/client/credentials.d.ts +5 -0
  27. package/build/autogestion/client/credentials.js +40 -0
  28. package/build/autogestion/client/install.d.ts +19 -0
  29. package/build/autogestion/client/install.js +91 -0
  30. package/build/autogestion/client/installed-systems.d.ts +7 -0
  31. package/build/autogestion/client/installed-systems.js +58 -0
  32. package/build/autogestion/client/local-session.d.ts +17 -0
  33. package/build/autogestion/client/local-session.js +37 -0
  34. package/build/autogestion/client/session.d.ts +18 -0
  35. package/build/autogestion/client/session.js +1 -0
  36. package/build/autogestion/client/stored-session.d.ts +42 -0
  37. package/build/autogestion/client/stored-session.js +126 -0
  38. package/build/autogestion/client/types.d.ts +49 -0
  39. package/build/autogestion/client/types.js +1 -0
  40. package/build/autogestion/format/correlatividades.d.ts +4 -0
  41. package/build/autogestion/format/correlatividades.js +4 -0
  42. package/build/autogestion/format/estado-academico.d.ts +4 -0
  43. package/build/autogestion/format/estado-academico.js +4 -0
  44. package/build/autogestion/format/examenes.d.ts +3 -0
  45. package/build/autogestion/format/examenes.js +8 -0
  46. package/build/autogestion/format/notas-parciales.d.ts +3 -0
  47. package/build/autogestion/format/notas-parciales.js +8 -0
  48. package/build/autogestion/link-page.d.ts +7 -0
  49. package/build/autogestion/link-page.js +62 -0
  50. package/build/autogestion/login.d.ts +11 -0
  51. package/build/autogestion/login.js +59 -0
  52. package/build/autogestion/tools.d.ts +3 -0
  53. package/build/autogestion/tools.js +73 -0
  54. package/build/http.d.ts +26 -0
  55. package/build/http.js +7 -2
  56. package/build/index.d.ts +2 -0
  57. package/build/index.js +35 -4
  58. package/build/oauth/access.d.ts +18 -0
  59. package/build/oauth/admin.d.ts +8 -0
  60. package/build/oauth/admin.js +1 -1
  61. package/build/oauth/pending-login.d.ts +20 -0
  62. package/build/oauth/redirects.d.ts +20 -0
  63. package/build/oauth/server.d.ts +22 -0
  64. package/build/oauth/server.js +1 -1
  65. package/build/server.d.ts +11 -0
  66. package/build/server.js +19 -2
  67. package/build/shared/documents/index.d.ts +33 -0
  68. package/build/shared/documents/merge.d.ts +32 -0
  69. package/build/shared/documents/office.d.ts +9 -0
  70. package/build/shared/documents/pdf.d.ts +2 -0
  71. package/build/shared/documents/png.d.ts +2 -0
  72. package/build/shared/documents/zip.d.ts +18 -0
  73. package/build/shared/expiring.d.ts +26 -0
  74. package/build/{oauth → shared}/expiring.js +4 -3
  75. package/build/shared/html.d.ts +6 -0
  76. package/build/shared/html.js +7 -1
  77. package/build/shared/http.d.ts +6 -0
  78. package/build/shared/input.d.ts +7 -0
  79. package/build/shared/load-env.d.ts +1 -0
  80. package/build/shared/logger.d.ts +5 -0
  81. package/build/shared/pages.d.ts +6 -0
  82. package/build/shared/paths.d.ts +10 -0
  83. package/build/shared/store/session-store.d.ts +77 -0
  84. package/build/shared/store/sqlite-store.d.ts +36 -0
  85. package/build/shared/uploads.d.ts +62 -0
  86. package/package.json +8 -1
package/README.md CHANGED
@@ -29,6 +29,10 @@ El agente recorre el aula como lo harías vos: entra al curso, mira los foros y
29
29
  | `aula_choice_answer` | Responde una elección o te anota en un grupo. |
30
30
  | `aula_send_message` | Manda un mensaje privado a un docente o compañero. |
31
31
  | `aula_calendar_add_event` | Agrega un recordatorio a tu calendario del aula. |
32
+ | `autogestion_cursando_ahora` | Las materias que estás cursando ahora según la autogestión, con comisión, aula y horarios cuando el sistema los publica. Incluye electivas. Aparece solo si tu regional tiene un conector de autogestión. |
33
+ | `autogestion_estado_academico` | Tu estado académico según la autogestión de tu regional: cada materia con su estado, plan, nota y fecha. Aparece solo si tu regional tiene un conector de autogestión. |
34
+ | `autogestion_correlatividades` | Las correlatividades de tu carrera según la autogestión, para cursar y para rendir, en vez de datos fijos del repo. Aparece solo si tu regional tiene un conector de autogestión. |
35
+ | `autogestion_examenes` | Tu historial de finales según la autogestión: fecha, materia, nota o estado, plan y código. Aparece solo si tu regional tiene un conector de autogestión. |
32
36
 
33
37
  Al abrir una tarea, `aula_open` muestra si la entregaste, cuándo vence y la devolución con la nota. Las tools que escriben le piden al agente que te muestre lo que va a hacer y espere tu confirmación.
34
38
 
@@ -134,6 +138,15 @@ Aparece como una opción más al iniciar sesión.
134
138
 
135
139
  `UTN_REGIONAL` (por ejemplo, `UTN_REGIONAL=frt`) elige la que viene marcada por defecto, y es la que se usa si en lugar de `login` definís `UTN_USER` y `UTN_PASS`.
136
140
 
141
+ ### Autogestión (Sysacad)
142
+ Si tu regional usa el Sysacad clásico, al terminar `login` te pregunta si querés conectarlo también. Si decís que sí, baja el conector de Sysacad (no viene con el paquete: si no lo usás, no se baja), te pide tu legajo y tu contraseña de Sysacad, que no siempre son las del aula, los prueba y los guarda en el gestor de credenciales. Para conectarlo después, o cambiar la contraseña:
143
+ ```bash
144
+ npx -y utn-mcp login autogestion
145
+ ```
146
+ Con Sysacad conectado aparecen las tools `autogestion_*`: tu estado académico, lo que estás cursando, tus finales y las correlatividades de tu carrera. Por ahora son solo de lectura.
147
+
148
+ Lo usan Bahía Blanca, Chubut, Concordia, Delta, General Pacheco, Haedo, La Rioja, Mar del Plata, Neuquén, Paraná, Reconquista, Santa Fe, San Francisco, San Nicolás, San Rafael, Tucumán, Tierra del Fuego y Venado Tuerto. En Tucumán anduvo con una versión anterior del conector; esta todavía no se probó con una cuenta real: si lo probás, contanos en un [issue](https://github.com/nialvape/utn-mcp/issues) si funcionó. En algunas el usuario no es el legajo tal cual (en Delta va sin el "23-", por ejemplo); el login te dice qué escribir. Avellaneda y Rosario quedan afuera porque su Sysacad pide un captcha o un desafío del navegador, y Rafaela, Santa Cruz y Trenque Lauquen porque no tienen HTTPS válido y la contraseña viajaría expuesta.
149
+
137
150
  ### Modo HTTP (opcional)
138
151
  Necesita Node 22.13 o más nuevo. Primero, una vez:
139
152
  ```bash
@@ -150,6 +163,8 @@ En este modo el servidor no lee archivos de tu disco: para entregar, el agente p
150
163
  ### Hostearlo para otros
151
164
  En modo HTTP el servidor también es servidor de autorización OAuth: un cliente MCP (Claude, ChatGPT) que se conecta a `https://tu-dominio/mcp` recibe un 401, descubre el OAuth y abre una página donde el alumno elige su regional y entra con su usuario de la UTN. Cada alumno usa el aula de su regional, aunque sean de regionales distintas. Ese login vincula su aula: la contraseña se usa una sola vez para sacar el token de Moodle y **no se guarda**. Cuando la sesión del aula vence, el agente le pasa el link de `/login` para renovarla, sin reconectar el cliente.
152
165
 
166
+ La autogestión (Sysacad) funciona igual para cada alumno, según su regional. Al arrancar, el servidor instala los conectores del catálogo en `connectors/` de su carpeta de datos. La primera vez que el agente la necesita, le pasa al alumno un link a `/autogestion/...` para que entre con su legajo y contraseña de Sysacad desde el navegador: la contraseña nunca pasa por el chat. Como con el aula, la contraseña se usa una sola vez y **no se guarda**: queda cifrada en la base solo la sesión de Sysacad. Cuando Sysacad la vence (unos minutos sin usarla), el agente le vuelve a pasar el link.
167
+
153
168
  Quién puede entrar lo decidís vos: con `UTN_MCP_ALLOWED_EMAILS` (una lista), o con `UTN_MCP_ACCESS_URL`, que apunta a un servicio tuyo. El servidor le hace `POST` con `{ "email", "usuario", "regional" }` y el header `Authorization: Bearer <UTN_MCP_ACCESS_SECRET>`, y espera `{ "permitido": true }` o `{ "permitido": false, "motivo": "...", "url": "..." }`. Se consulta al conectar y en cada renovación del token (cada hora), así que una baja se aplica sola.
154
169
 
155
170
  La configuración va en variables de entorno o en un `.env` en la carpeta desde donde lo corrés (copiá [`.env.example`](https://github.com/nialvape/utn-mcp/blob/main/.env.example)):
@@ -179,11 +194,12 @@ Guardá esa `UTN_MCP_KEY`: sin ella no se pueden descifrar las sesiones guardada
179
194
  Instalado en tu computadora, todo queda ahí:
180
195
  - **Usuario y contraseña**: en el gestor de credenciales del sistema (Windows Credential Manager, Keychain en macOS, Secret Service en Linux).
181
196
  - **Token del aula virtual**: en `%APPDATA%\utn-mcp\session.json` (Windows) o `~/.config/utn-mcp/session.json` (Linux y macOS).
182
- - **En modo HTTP**: la contraseña no se guarda; la sesión va cifrada en `utn-mcp.db`, en la misma carpeta, con la clave de `store.key` (o la de `UTN_MCP_KEY`). Del token del cliente solo se guarda el hash.
197
+ - **Conector de autogestión**: en `connectors/`, dentro de esa misma carpeta. El legajo y la contraseña de Sysacad van al gestor de credenciales, aparte de los del aula.
198
+ - **En modo HTTP**: la contraseña no se guarda; la sesión va cifrada en `utn-mcp.db`, en la misma carpeta, con la clave de `store.key` (o la de `UTN_MCP_KEY`). Del token del cliente solo se guarda el hash. Con la de Sysacad pasa lo mismo: no se guarda, y su sesión va cifrada en la misma base.
183
199
 
184
- En Linux sin gestor de credenciales (servidores, Docker, WSL) podés definir las variables de entorno `UTN_USER` y `UTN_PASS` en lugar de usar `login`.
200
+ En Linux sin gestor de credenciales (servidores, Docker, WSL) podés definir las variables de entorno `UTN_USER` y `UTN_PASS` en lugar de usar `login`. Para Sysacad son `SYSACAD_ASP_USER` y `SYSACAD_ASP_PASS`.
185
201
 
186
- Si usás un servidor hosteado por otra persona, tu sesión del aula la guarda ese servidor (cifrada, y sin tu contraseña): revisá su política de privacidad.
202
+ Si usás un servidor hosteado por otra persona, tus sesiones del aula y de Sysacad las guarda ese servidor (cifradas, y sin tus contraseñas): revisá su política de privacidad.
187
203
 
188
204
  ## Desarrollo
189
205
  ```bash
@@ -197,5 +213,16 @@ npm run build # compila a build/
197
213
 
198
214
  pdf.js no es una dependencia del paquete publicado. Está como `devDependency`, y `npm install` corre el script `prepare`, que copia a `vendor/pdfjs/` solo los archivos que usa el servidor (~4 MB). Así, quien instala `utn-mcp` no baja pdf.js entero ni su canvas nativo opcional (~37 MB). Si instalaste con `--ignore-scripts`, generalo a mano con `npm run prepare`.
199
215
 
216
+ ### Conectores de autogestión
217
+ Cada sistema de autogestión es un paquete aparte en `packages/` (hoy, `packages/sysacad-asp`). No se publica en npm: `utn-mcp login` baja el `.tgz` adjunto a un release del repo, comprueba que su hash sea el de `src/autogestion/client/catalog.ts` y lo extrae en `connectors/`. El conector implementa `AutogestionSystem` de `src/autogestion/client/connector.ts` y del core importa solo tipos: lo que necesita en tiempo de ejecución (las clases de error, `htmlToText`) se lo pasa el core en `create`.
218
+
219
+ Para publicar una versión de un conector:
220
+ ```bash
221
+ node scripts/pack-connector.mjs sysacad-asp
222
+ ```
223
+ Arma el `.tgz` en `build-connectors/` y muestra la URL y el hash que van al catálogo, y el `gh release create` que lo sube.
224
+
225
+ Para probar un conector sin publicarlo, compilalo, instalalo con `npm install` en la carpeta desde la que corrés `utn-mcp` y nombralo en `UTN_MCP_CONNECTORS` (separados por comas). Cargar un conector ejecuta su código: nombrá solo paquetes en los que confíes.
226
+
200
227
  ## Aviso
201
228
  Este proyecto no está afiliado ni avalado por la UTN. Usalo bajo tu responsabilidad.
@@ -0,0 +1,97 @@
1
+ import type { AulaSession } from "./session.js";
2
+ import type { Assignment, CourseModuleContent, AssignSubmissionStatus, Choice, ChoiceOption, Course, CourseSection, ForumDiscussion, ForumPost, SiteInfo } from "./types.js";
3
+ export declare function getSiteInfo(session: AulaSession): Promise<SiteInfo>;
4
+ /** El usuario dueño del token, con su email institucional (site_info no lo trae). */
5
+ export declare function getOwnUser(session: AulaSession): Promise<{
6
+ username: string;
7
+ fullname: string;
8
+ email: string | null;
9
+ }>;
10
+ export declare function getUserCourses(session: AulaSession): Promise<Course[]>;
11
+ /**
12
+ * Secciones del curso con sus módulos. Con `cmid` Moodle devuelve igual todas las secciones
13
+ * pero solo popula el módulo pedido, así que no hay que traerse el curso entero para abrir una cosa.
14
+ */
15
+ export declare function getCourseSections(session: AulaSession, courseId: number, cmid?: number): Promise<CourseSection[]>;
16
+ /** Un archivo de un curso, con dónde está: en qué sección y en qué módulo. */
17
+ export interface CourseFileEntry {
18
+ section: string;
19
+ module: string;
20
+ file: CourseModuleContent;
21
+ }
22
+ /**
23
+ * Los archivos de las secciones y módulos visibles del curso, en el orden en que aparecen en el aula.
24
+ * Los mismos filtros de visibilidad que el mapa del curso de format/navigate.
25
+ */
26
+ export declare function getCourseFiles(session: AulaSession, courseId: number): Promise<ReadonlyArray<CourseFileEntry>>;
27
+ /** Cuántas discusiones trae una página del índice de un foro. */
28
+ export declare const PAGE_SIZE = 50;
29
+ /**
30
+ * Discusiones de un foro, las más recientes primero. Moodle manda el mensaje completo de la
31
+ * primera publicación de cada una; el que lee decide si lo recorta.
32
+ */
33
+ export declare function getForumDiscussions(session: AulaSession, forumid: number, page?: number): Promise<{
34
+ discussions: ForumDiscussion[];
35
+ }>;
36
+ /** Publicaciones de una discusión, incluidas las respuestas. */
37
+ export declare function getDiscussionPosts(session: AulaSession, discussionid: number): Promise<{
38
+ posts: ForumPost[];
39
+ }>;
40
+ /** Si el usuario puede abrir un hilo en el foro, con el grupo que le toque. */
41
+ export declare function canAddDiscussion(session: AulaSession, forumid: number): Promise<boolean>;
42
+ /** Abre un hilo. Moodle elige solo el grupo del usuario si el foro es por grupos. */
43
+ export declare function addDiscussion(session: AulaSession, forumid: number, subject: string, message: string): Promise<number>;
44
+ /** Responde a una publicación, que puede ser la primera de la discusión. */
45
+ export declare function addDiscussionPost(session: AulaSession, postid: number, subject: string, message: string): Promise<number>;
46
+ /** Edita una publicación propia; con asunto vacío Moodle deja el que tenía. */
47
+ export declare function updateDiscussionPost(session: AulaSession, postid: number, subject: string, message: string): Promise<void>;
48
+ /** Borra una publicación; si es la primera, se va la discusión entera. */
49
+ export declare function deletePost(session: AulaSession, postid: number): Promise<void>;
50
+ /** Las tareas de un curso con su configuración: fechas y qué tipos de entrega aceptan. */
51
+ export declare function getAssignments(session: AulaSession, courseId: number): Promise<Assignment[]>;
52
+ /** Estado de la entrega del usuario y, si ya la corrigieron, la devolución. */
53
+ export declare function getSubmissionStatus(session: AulaSession, assignid: number): Promise<AssignSubmissionStatus>;
54
+ /** Un borrador vacío: el `itemid` que se manda para dejar un área sin archivos. */
55
+ export declare function getUnusedDraftItemId(session: AulaSession): Promise<number>;
56
+ /**
57
+ * Guarda la entrega. Cada plugin habilitado reemplaza lo que tenía con lo que se le pasa: el de
58
+ * archivos toma el borrador `filesItemId` entero y el de texto, `text`.
59
+ */
60
+ export declare function saveSubmission(session: AulaSession, assignmentid: number, data: {
61
+ filesItemId?: number;
62
+ text?: {
63
+ html: string;
64
+ format: number;
65
+ itemid: number;
66
+ };
67
+ }): Promise<void>;
68
+ /** Confirma el envío de un borrador. Después ya no se puede editar. */
69
+ export declare function submitForGrading(session: AulaSession, assignmentid: number, acceptStatement: boolean): Promise<void>;
70
+ /**
71
+ * Vacía la entrega del usuario (o la de su grupo). Es la única forma de dejarla sin nada:
72
+ * mod_assign_save_submission rechaza una entrega vacía.
73
+ */
74
+ export declare function removeSubmission(session: AulaSession, assignid: number): Promise<void>;
75
+ export declare function getChoices(session: AulaSession, courseId: number): Promise<Choice[]>;
76
+ export declare function getChoiceOptions(session: AulaSession, choiceid: number): Promise<ChoiceOption[]>;
77
+ export declare function submitChoiceResponse(session: AulaSession, choiceid: number, optionIds: number[]): Promise<void>;
78
+ export declare function deleteChoiceResponses(session: AulaSession, choiceid: number): Promise<void>;
79
+ export declare function getChoicegroupOptions(session: AulaSession, choicegroupid: number): Promise<ChoiceOption[]>;
80
+ /**
81
+ * El plugin lee `responses` si la actividad deja un solo grupo y `choicegroup_<id>: "true"` si deja
82
+ * varios, y la configuración no se puede consultar: mandamos las dos formas y él usa la que le sirve.
83
+ * Con varios grupos, los que no vienen marcados se quitan.
84
+ */
85
+ export declare function submitChoicegroupResponse(session: AulaSession, choicegroupid: number, optionIds: number[]): Promise<void>;
86
+ export declare function deleteChoicegroupResponses(session: AulaSession, choicegroupid: number): Promise<void>;
87
+ /** Nombre de otro usuario, visto desde el usuario logueado, o null si no existe. */
88
+ export declare function getMemberName(session: AulaSession, userId: number): Promise<string | null>;
89
+ /** Manda un mensaje privado. Moodle no tira excepción si falla: devuelve `msgid: -1` y el motivo. */
90
+ export declare function sendInstantMessage(session: AulaSession, touserid: number, html: string): Promise<number>;
91
+ /** Crea un evento personal: solo lo ve el usuario. */
92
+ export declare function createUserEvent(session: AulaSession, event: {
93
+ name: string;
94
+ description: string;
95
+ timestart: number;
96
+ timeduration: number;
97
+ }): Promise<number>;
@@ -0,0 +1,10 @@
1
+ export interface Credentials {
2
+ username: string;
3
+ password: string;
4
+ /** Falta en UTN_USER/UTN_PASS y en las guardadas antes de elegir la regional al entrar. */
5
+ regional?: string;
6
+ }
7
+ /** Primero UTN_USER/UTN_PASS; si no están, el keychain del sistema. */
8
+ export declare function loadCredentials(): Promise<Credentials | null>;
9
+ /** Guarda en el keychain. Devuelve false si no hay keychain (ej. Linux sin Secret Service). */
10
+ export declare function saveCredentials(creds: Credentials): Promise<boolean>;
@@ -0,0 +1,21 @@
1
+ import type { SessionStore } from "../../shared/store/session-store.js";
2
+ import type { Credentials } from "./credentials.js";
3
+ import type { Regional } from "./regionals.js";
4
+ import { type SecondFactor } from "./sso-login.js";
5
+ export interface LinkedAccount {
6
+ userId: number;
7
+ regional: Regional;
8
+ username: string;
9
+ fullname: string;
10
+ email: string | null;
11
+ /** Este login fue el primero en verificar la regional en este servidor. */
12
+ firstVerifier: boolean;
13
+ }
14
+ /**
15
+ * Entra al aula de la regional con usuario y contraseña, crea el usuario del store si no existe y le
16
+ * guarda la sesión (el token). La contraseña se usa solo acá: no se guarda en ningún lado.
17
+ * Lo usan `utn-mcp login --http`, el login por OAuth de la versión hosteada y `/admin/verificar`.
18
+ * `regional` tiene que venir con su soporte en este servidor (`availableRegionals` con las verificadas).
19
+ * `secondFactor` contesta el MFA de Microsoft si la cuenta lo pide.
20
+ */
21
+ export declare function linkAulaAccount(store: SessionStore, regional: Regional, credentials: Credentials, secondFactor?: SecondFactor): Promise<LinkedAccount>;
@@ -0,0 +1,23 @@
1
+ import { type Credentials } from "./credentials.js";
2
+ import { type Regional } from "./regionals.js";
3
+ import type { AulaSession } from "./session.js";
4
+ import { type SecondFactor } from "./sso-login.js";
5
+ /**
6
+ * Sesión del modo local: un solo usuario, guardada en disco.
7
+ * Credenciales en el keychain (o UTN_USER/UTN_PASS); el token y la regional en session.json.
8
+ */
9
+ export declare class LocalSession implements AulaSession {
10
+ private refreshing;
11
+ /**
12
+ * La elegida en `utn-mcp login`: la de session.json o, si se borró, la guardada con las credenciales.
13
+ * Sin login (UTN_USER/UTN_PASS) o de antes de elegirla, la de UTN_REGIONAL.
14
+ */
15
+ getRegional(): Promise<Regional>;
16
+ getToken(): Promise<string>;
17
+ /** Una sola renovación en curso aunque varias tools fallen a la vez. */
18
+ refreshToken(): Promise<string>;
19
+ /** Obtiene un token nuevo y lo guarda. Sin `secondFactor` (al renovar solo), una cuenta con MFA no entra. */
20
+ login(regional: Regional, credentials: Credentials | null, secondFactor?: SecondFactor): Promise<string>;
21
+ private read;
22
+ private write;
23
+ }
@@ -0,0 +1,25 @@
1
+ import type { AulaSession } from "./session.js";
2
+ export declare class MoodleError extends Error {
3
+ readonly errorcode?: string | undefined;
4
+ constructor(message: string, errorcode?: string | undefined);
5
+ }
6
+ /** Llama a una función del web service de Moodle; renueva el token una vez si venció. */
7
+ export declare function callWs<T>(session: AulaSession, wsfunction: string, params?: Record<string, string | number>): Promise<T>;
8
+ /**
9
+ * Baja un archivo de `webservice/pluginfile.php` con el token; lo renueva una vez si venció.
10
+ * Los errores llegan como JSON (`{ error, errorcode }`) en lugar del archivo.
11
+ * Los redirects se siguen a mano y SOLO al propio aula y a rutas de archivos (pluginfile):
12
+ * un pluginfile legítimo no redirige afuera, así que cualquier otro destino se rechaza.
13
+ */
14
+ export declare function fetchFile(session: AulaSession, url: URL, maxBytes: number): Promise<{
15
+ bytes: Uint8Array;
16
+ contentType: string | null;
17
+ }>;
18
+ /**
19
+ * Sube archivos al área de borrador del usuario por `webservice/upload.php` y devuelve su `itemid`,
20
+ * que es lo que piden las funciones que guardan archivos. Todos quedan en el mismo borrador.
21
+ */
22
+ export declare function uploadDraftFiles(session: AulaSession, files: {
23
+ name: string;
24
+ bytes: Uint8Array;
25
+ }[]): Promise<number>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Qué se sabe de si el MCP anda con el aula de una regional:
3
+ * - `verified`: alguien ya entró y usó las tools.
4
+ * - `candidate`: tiene habilitado el servicio `moodle_mobile_app` (y se sabe cómo entra), así que
5
+ * casi seguro anda; se verifica con el primer login que funcione.
6
+ * - `unsupported`: no tiene el servicio habilitado, o entra por un login que todavía no sabemos usar;
7
+ * se puede elegir, pero todavía no se puede usar.
8
+ */
9
+ export type RegionalSupport = "verified" | "candidate" | "unsupported";
10
+ /** Los SSO que sabemos usar: Keycloak propio de la regional u Office 365 (Microsoft Entra ID). */
11
+ export type SsoProviderId = "keycloak" | "microsoft";
12
+ /** El SSO por el que entra el aula: quién valida usuario y contraseña, y en qué host. */
13
+ export interface Sso {
14
+ provider: SsoProviderId;
15
+ host: string;
16
+ }
17
+ /** El aula virtual de una regional de la UTN. Sin `sso`, entra con el login directo de Moodle. */
18
+ export interface Regional {
19
+ id: string;
20
+ name: string;
21
+ baseUrl: string;
22
+ sso?: Sso;
23
+ support: RegionalSupport;
24
+ }
25
+ /** `UTN_REGIONAL` no está en el registro: sin cortar acá, las credenciales irían al SSO de FRBA. */
26
+ export declare class UnknownRegionalError extends Error {
27
+ name: string;
28
+ constructor(regional: string);
29
+ }
30
+ /** Su aula no tiene el servicio habilitado o entra por un SSO que no sabemos usar: ni se intenta entrar. */
31
+ export declare class UnsupportedRegionalError extends Error {
32
+ readonly regional: Regional;
33
+ name: string;
34
+ constructor(regional: Regional);
35
+ }
36
+ /**
37
+ * Las que se pueden elegir al iniciar sesión: el catálogo y, si está definida, la de UTN_AULA_BASE_URL.
38
+ * `verifiedIds` son las `candidate` que ya se verificaron en el servidor (las guarda el store).
39
+ */
40
+ export declare function availableRegionals(verifiedIds?: Iterable<string>): Regional[];
41
+ /** La regional con ese id; si no está entre las disponibles, `UnknownRegionalError`. */
42
+ export declare function findRegional(id: string): Regional;
43
+ /**
44
+ * La que viene elegida si nadie dice otra: la de UTN_REGIONAL; si no, la de UTN_AULA_BASE_URL; si no,
45
+ * FRBA. Es la de quien usa UTN_USER/UTN_PASS sin `utn-mcp login`, y la opción por defecto al elegir.
46
+ */
47
+ export declare function defaultRegional(): Regional;
@@ -0,0 +1,12 @@
1
+ import type { Regional } from "./regionals.js";
2
+ /**
3
+ * El aula y el token de un usuario. Todo lo que habla con el aula recibe esto y no sabe de dónde sale:
4
+ * `LocalSession` (STDIO, el usuario de la compu) o `StoredAulaSession` (HTTP, un usuario del store).
5
+ */
6
+ export interface AulaSession {
7
+ getRegional(): Promise<Regional>;
8
+ /** Token vigente; si no hay ninguno, obtiene uno. */
9
+ getToken(): Promise<string>;
10
+ /** Descarta el token actual y obtiene uno nuevo. */
11
+ refreshToken(): Promise<string>;
12
+ }
@@ -0,0 +1,60 @@
1
+ import type { Credentials } from "./credentials.js";
2
+ import { type Regional } from "./regionals.js";
3
+ export declare class LoginRequiredError extends Error {
4
+ constructor(message?: string);
5
+ }
6
+ export declare class InvalidCredentialsError extends Error {
7
+ constructor(reason?: string);
8
+ }
9
+ /**
10
+ * Obtiene un token del web service. Dos estrategias según la regional:
11
+ * - Regionales con SSO (Keycloak como FRBA, Office 365 como FRH): flujo de launch.php, que usa la
12
+ * sesión del SSO. Si el SSO rechaza las credenciales, `login/token.php`: hay usuarios sin cuenta en
13
+ * el SSO.
14
+ * - Regionales con login directo de Moodle (ej. FRT): primero `login/token.php` (no depende del
15
+ * plugin de mobile launch, que puede estar deshabilitado); si falla, flujo de launch.php.
16
+ * Las cookies se usan solo durante el login y se descartan: las sesiones de Moodle y del SSO duran
17
+ * horas y el token, semanas, así que no sirven para renovarlo.
18
+ * `secondFactor` contesta el MFA de Microsoft; sin él (las renovaciones solas), una cuenta con MFA da
19
+ * `MfaRequiredError`.
20
+ */
21
+ export declare function fetchToken(regional: Regional, credentials: Credentials, secondFactor?: SecondFactor): Promise<string>;
22
+ /** La cuenta pide MFA y no hay quién lo conteste: pasa al renovar solo, sin el usuario delante. */
23
+ export declare class MfaRequiredError extends LoginRequiredError {
24
+ constructor();
25
+ }
26
+ /** El segundo factor no se pudo completar; el mensaje dice por qué y se le puede mostrar al usuario. */
27
+ export declare class MfaError extends Error {
28
+ constructor(message: string);
29
+ }
30
+ /** Un método de segundo factor de la cuenta de Microsoft que sabemos completar. */
31
+ export interface MfaMethod {
32
+ /** El `authMethodId` de Microsoft (ej. `PhoneAppNotification`, `OneWaySMS`). */
33
+ id: string;
34
+ /** "approve": se aprueba en el celular (notificación o llamada); "code": se tipea un código. */
35
+ kind: "approve" | "code";
36
+ /** Para mostrarle al usuario, con el teléfono enmascarado si Microsoft lo da (ej. "Código por SMS · +XX XXXXXXXX12"). */
37
+ label: string;
38
+ }
39
+ /** Lo que el login le pregunta al usuario para completar el segundo factor. */
40
+ export type MfaPrompt = {
41
+ step: "choose";
42
+ methods: MfaMethod[];
43
+ } | {
44
+ step: "code";
45
+ method: MfaMethod;
46
+ retry: boolean;
47
+ }
48
+ /** `number` es el que Authenticator pide tipear al aprobar, si lo pide. */
49
+ | {
50
+ step: "approve";
51
+ method: MfaMethod;
52
+ number?: number;
53
+ };
54
+ /**
55
+ * Quien contesta el segundo factor: el CLI o la página de login del servidor. Con "choose" devuelve el
56
+ * `id` del método; con "code", el código; con "approve", cualquier cosa cuando el usuario ya aprobó.
57
+ */
58
+ export type SecondFactor = (prompt: MfaPrompt) => Promise<string>;
59
+ /** moodlemobile://token=BASE64(siteid:::token[:::privatetoken]) → token */
60
+ export declare function decodeLaunchToken(url: string): string;
@@ -0,0 +1,32 @@
1
+ import type { SessionStore } from "../../shared/store/session-store.js";
2
+ import { type Regional } from "./regionals.js";
3
+ import type { AulaSession } from "./session.js";
4
+ /** Con qué nombre guarda el aula su sesión en el store. */
5
+ export declare const AULA_PLATFORM = "aula";
6
+ /** Lo que el aula guarda en el store para un usuario. */
7
+ export interface StoredAulaData {
8
+ username: string;
9
+ token: string;
10
+ }
11
+ /**
12
+ * Sesión del modo HTTP: la de un usuario del store, en el aula de su regional. Renueva el token con la
13
+ * contraseña del keychain cuando es de este mismo usuario: eso pasa solo en local, porque el servidor
14
+ * hosteado no guarda contraseñas. Si no, el usuario vuelve a iniciar sesión.
15
+ */
16
+ export declare class StoredAulaSession implements AulaSession {
17
+ private readonly store;
18
+ private readonly userId;
19
+ private readonly loginUrl?;
20
+ private refreshing;
21
+ private regional;
22
+ /** `loginUrl` es la página donde el usuario vuelve a iniciar sesión, si el servidor tiene una. */
23
+ constructor(store: SessionStore, userId: number, loginUrl?: string | undefined);
24
+ /** La del usuario en el store; no cambia mientras dura la sesión, así que se lee una sola vez. */
25
+ getRegional(): Promise<Regional>;
26
+ getToken(): Promise<string>;
27
+ refreshToken(): Promise<string>;
28
+ private renew;
29
+ private read;
30
+ /** Hosteado, el usuario vuelve a entrar por la página del servidor; sin ella, por el CLI. */
31
+ private loginRequired;
32
+ }
@@ -0,0 +1,173 @@
1
+ export interface SiteInfo {
2
+ sitename: string;
3
+ username: string;
4
+ fullname: string;
5
+ userid: number;
6
+ release: string;
7
+ functions: {
8
+ name: string;
9
+ version: string;
10
+ }[];
11
+ }
12
+ export interface Course {
13
+ id: number;
14
+ shortname: string;
15
+ fullname: string;
16
+ visible: number;
17
+ progress?: number | null;
18
+ lastaccess?: number | null;
19
+ }
20
+ export interface CourseModuleContent {
21
+ type: "file" | "url" | "content";
22
+ filename: string;
23
+ filepath: string | null;
24
+ filesize: number;
25
+ fileurl: string;
26
+ timemodified: number | null;
27
+ mimetype?: string;
28
+ }
29
+ export interface CourseModule {
30
+ id: number;
31
+ name: string;
32
+ modname: string;
33
+ /** id del plugin, no del módulo: es lo que piden las funciones mod_* (p. ej. forumid). */
34
+ instance: number;
35
+ url?: string;
36
+ description?: string;
37
+ uservisible: boolean;
38
+ availabilityinfo?: string;
39
+ dates?: {
40
+ label: string;
41
+ timestamp: number;
42
+ }[];
43
+ contents?: CourseModuleContent[];
44
+ }
45
+ export interface CourseSection {
46
+ id: number;
47
+ name: string;
48
+ section: number;
49
+ summary: string;
50
+ uservisible?: boolean;
51
+ modules: CourseModule[];
52
+ }
53
+ export interface ForumDiscussion {
54
+ /** id de la discusión; es el que pide mod_forum_get_discussion_posts. */
55
+ discussion: number;
56
+ subject: string;
57
+ message: string;
58
+ userfullname: string;
59
+ created: number;
60
+ timemodified: number;
61
+ numreplies: number;
62
+ userid: number;
63
+ pinned: boolean;
64
+ attachment: boolean;
65
+ }
66
+ export interface ForumPost {
67
+ id: number;
68
+ subject: string;
69
+ /** El asunto que Moodle propone para responderla ("Re: ..."). */
70
+ replysubject: string;
71
+ message: string;
72
+ author: {
73
+ id: number;
74
+ fullname: string;
75
+ };
76
+ timecreated: number;
77
+ parentid: number | null;
78
+ isdeleted: boolean;
79
+ capabilities: {
80
+ reply: boolean;
81
+ edit: boolean;
82
+ delete: boolean;
83
+ };
84
+ attachments: {
85
+ filename: string;
86
+ fileurl: string;
87
+ filesize: number;
88
+ mimetype?: string;
89
+ }[];
90
+ }
91
+ export interface MoodleFile {
92
+ filename: string;
93
+ filepath?: string | null;
94
+ filesize: number;
95
+ fileurl: string;
96
+ mimetype?: string;
97
+ timemodified?: number | null;
98
+ }
99
+ /** Un plugin de entrega o de devolución: archivos (`fileareas`) o texto (`editorfields`). */
100
+ export interface AssignPlugin {
101
+ type: string;
102
+ name: string;
103
+ fileareas?: {
104
+ area: string;
105
+ files?: MoodleFile[];
106
+ }[];
107
+ editorfields?: {
108
+ name: string;
109
+ text: string;
110
+ format: number;
111
+ }[];
112
+ }
113
+ export interface AssignSubmission {
114
+ status: "new" | "draft" | "submitted" | "reopened" | string;
115
+ timemodified: number;
116
+ plugins?: AssignPlugin[];
117
+ }
118
+ export interface AssignSubmissionStatus {
119
+ lastattempt?: {
120
+ submission?: AssignSubmission;
121
+ /** En las entregas grupales la que cuenta es esta, no la individual. */
122
+ teamsubmission?: AssignSubmission;
123
+ locked: boolean;
124
+ graded: boolean;
125
+ canedit: boolean;
126
+ cansubmit: boolean;
127
+ extensionduedate: number | null;
128
+ gradingstatus: string;
129
+ };
130
+ feedback?: {
131
+ gradefordisplay?: string;
132
+ gradeddate?: number;
133
+ plugins?: AssignPlugin[];
134
+ };
135
+ }
136
+ export interface Assignment {
137
+ id: number;
138
+ cmid: number;
139
+ duedate: number;
140
+ cutoffdate: number;
141
+ allowsubmissionsfromdate: number;
142
+ /** 1: la entrega queda en borrador hasta que se confirma el envío. */
143
+ submissiondrafts: number;
144
+ requiresubmissionstatement: number;
145
+ /** HTML de la declaración que hay que aceptar al enviar; solo viene si se pide. */
146
+ submissionstatement?: string;
147
+ teamsubmission: number;
148
+ maxattempts: number;
149
+ configs: {
150
+ plugin: string;
151
+ subtype: string;
152
+ name: string;
153
+ value: string;
154
+ }[];
155
+ }
156
+ export interface Choice {
157
+ id: number;
158
+ coursemodule: number;
159
+ allowupdate: boolean;
160
+ allowmultiple: boolean;
161
+ timeopen: number;
162
+ timeclose: number;
163
+ }
164
+ export interface ChoiceOption {
165
+ id: number;
166
+ /** En choice es el texto de la opción; en choicegroup viene en `name`. */
167
+ text?: string;
168
+ name?: string;
169
+ maxanswers?: number;
170
+ countanswers?: number;
171
+ checked?: boolean;
172
+ disabled?: boolean;
173
+ }
@@ -0,0 +1,57 @@
1
+ import type { AulaSession } from "../client/session.js";
2
+ /**
3
+ * Con la dirección de un foro abre un hilo; con la de una discusión responde. Así el modelo no elige
4
+ * entre dos operaciones: la dirección que ya tiene dice cuál es.
5
+ */
6
+ export declare function postToForum(session: AulaSession, rawAddress: string, message: string, subject?: string, replyTo?: number): Promise<{
7
+ publicado: string;
8
+ direccion: string;
9
+ id?: undefined;
10
+ respuestaA?: undefined;
11
+ } | {
12
+ publicado: string;
13
+ direccion: string;
14
+ id: number;
15
+ respuestaA: number;
16
+ }>;
17
+ export declare function editForumPost(session: AulaSession, rawAddress: string, postId: number, message: string, subject?: string): Promise<{
18
+ editado: boolean;
19
+ direccion: string;
20
+ id: number;
21
+ }>;
22
+ export declare function deleteForumPost(session: AulaSession, rawAddress: string, postId: number): Promise<{
23
+ borrado: boolean;
24
+ id: number;
25
+ discusionBorrada: true | undefined;
26
+ direccion: string | undefined;
27
+ }>;
28
+ /** Deja elegidas exactamente `optionIds`; vacío borra la respuesta. Devuelve cómo quedó. */
29
+ export declare function answerChoice(session: AulaSession, rawAddress: string, optionIds: number[]): Promise<{
30
+ elegidas: number[];
31
+ aviso: string | undefined;
32
+ opciones: {
33
+ id: number;
34
+ texto: string | undefined;
35
+ elegida: true | undefined;
36
+ deshabilitada: true | undefined;
37
+ cupo: number | undefined;
38
+ anotados: number | undefined;
39
+ }[];
40
+ }>;
41
+ export declare function sendMessage(session: AulaSession, userId: number, message: string): Promise<{
42
+ enviado: boolean;
43
+ para: string;
44
+ }>;
45
+ export declare function addCalendarEvent(session: AulaSession, event: {
46
+ name: string;
47
+ start: string;
48
+ durationMinutes?: number;
49
+ description?: string;
50
+ }): Promise<{
51
+ creado: boolean;
52
+ id: number;
53
+ nombre: string;
54
+ inicio: string | null;
55
+ fin: string | null | undefined;
56
+ url: string;
57
+ }>;