blackboard-upc 1.0.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,60 @@
1
+ # Changelog
2
+
3
+ All notable changes to `blackboard-upc` will be documented here.
4
+
5
+ ---
6
+
7
+ ## [1.0.0] — 2026-03-30
8
+
9
+ ### Added
10
+
11
+ #### Autenticación
12
+ - Login via **SAML SSO → Microsoft Azure AD** con Playwright (ventana del browser)
13
+ - Sesión persistida en `~/.blackboard-cli/session.json` (TTL 8h, permisos 600)
14
+ - Comandos `login`, `logout`, `whoami`, `status`
15
+
16
+ #### Cursos
17
+ - `courses list` — cursos inscritos con nombre, rol, estado y último acceso
18
+ - `courses get <id>` — detalle de un curso
19
+ - `courses contents <id>` — árbol de contenido navegable por carpetas
20
+ - `courses contents --type file|folder|assignment|document` — filtro por tipo
21
+ - `courses announcements <id>` — anuncios del curso
22
+ - `courses grades <id>` — notas del ciclo
23
+
24
+ #### Tareas
25
+ - `assignments list <id>` — tareas con fecha de entrega, nota actual y alertas de color
26
+ - `assignments list --pending` — solo las pendientes de entrega
27
+ - `assignments attempts <id> <columnId>` — historial de entregas
28
+ - `assignments submit` — entregar tarea con archivo (`-f`), texto (`-t`) o borrador (`--draft`)
29
+
30
+ #### Descargas
31
+ - `download <courseId> <contentId>` — descargar archivo adjunto individual
32
+ - `download-folder <courseId> <folderId>` — descarga recursiva de toda una carpeta
33
+ - `download-folder --filter <keyword>` — filtrar por nombre de archivo
34
+
35
+ #### API & Developer experience
36
+ - `api <METHOD> <path>` — passthrough a cualquier endpoint de la REST API
37
+ - `endpoints` — catálogo documentado de 22+ endpoints con parámetros
38
+ - Todos los comandos aceptan `--json` con spinners redirigidos a `stderr`
39
+
40
+ #### MCP Server
41
+ - Comando `mcp` — inicia un servidor MCP (stdio) para Claude Code y Claude Desktop
42
+ - 13 herramientas: `whoami`, `list_courses`, `get_course`, `list_contents`,
43
+ `list_announcements`, `list_assignments`, `list_attempts`, `get_grades`,
44
+ `list_attachments`, `download_attachment`, `submit_attempt`, `raw_api`, `system_version`
45
+ - `CLAUDE.md` — guía de comportamiento para agentes IA
46
+
47
+ #### UI
48
+ - Banner ASCII con color rojo UPC (`#E31837`)
49
+ - Prompt "¿Qué puedo hacer ahora?" tras login exitoso
50
+ - Paleta semántica: `ok` (verde), `fail` (rojo), `warn` (amarillo), `hint` (cyan)
51
+
52
+ ---
53
+
54
+ ## Roadmap
55
+
56
+ - [ ] `npx` install sin clonar repo (publicación en npm)
57
+ - [ ] Refresh automático de sesión antes de expirar
58
+ - [ ] Notificaciones de entregas próximas (`assignments due`)
59
+ - [ ] Descarga de videos de grabaciones de clase
60
+ - [ ] Soporte para múltiples cuentas / ciclos simultáneos
package/CLAUDE.md ADDED
@@ -0,0 +1,70 @@
1
+ # blackboard-cli — Agent Guide
2
+
3
+ This CLI gives Claude direct access to UPC Aula Virtual (Blackboard Learn). Use it to help students check their courses, assignments, grades, and download materials — all without opening a browser.
4
+
5
+ ## Setup
6
+
7
+ Before using any tool, the user must be authenticated:
8
+
9
+ ```bash
10
+ blackboard login # opens browser for Microsoft SSO
11
+ blackboard whoami # verify session is active
12
+ ```
13
+
14
+ If you get `Not authenticated`, ask the user to run `blackboard login`.
15
+
16
+ ## Primary workflow
17
+
18
+ ```
19
+ 1. list_courses → find the relevant courseId
20
+ 2. list_assignments <courseId> → see pending tasks + due dates
21
+ 3. get_grades <courseId> → check current grades
22
+ 4. list_contents <courseId> → browse course materials
23
+ 5. list_contents <courseId> <parentId> → navigate into a subfolder
24
+ 6. list_attachments <courseId> <contentId>→ find downloadable files
25
+ ```
26
+
27
+ ## Agent behavior rules
28
+
29
+ - **Always confirm before submitting** (`submit_attempt`). Show the user what will be submitted and ask for confirmation. Never submit silently.
30
+ - **Show grades in context** — when showing grades, also show the assignment name, max score, and due date if available.
31
+ - **Navigate content recursively** — if the user asks for materials, explore subfolders using `list_contents` with `parentId`.
32
+ - **Use `raw_api` for anything not covered** — the Blackboard REST API is extensive. If there's no specific tool, use `raw_api` with the correct endpoint.
33
+ - **Session errors are recoverable** — if you get a session error, tell the user to run `blackboard login` (not a fatal error).
34
+ - **Respect rate limits** — don't fan out more than 5 parallel API calls.
35
+
36
+ ## Key IDs
37
+
38
+ Course IDs look like `_529580_1`. Content and column IDs follow the same pattern.
39
+
40
+ ## Useful endpoints (via raw_api)
41
+
42
+ ```
43
+ GET /learn/api/public/v1/users/me
44
+ GET /learn/api/public/v1/users/{userId}/courses
45
+ GET /learn/api/public/v1/courses/{courseId}/contents
46
+ GET /learn/api/public/v1/courses/{courseId}/contents/{id}/children
47
+ GET /learn/api/public/v1/courses/{courseId}/announcements
48
+ GET /learn/api/public/v2/courses/{courseId}/gradebook/columns
49
+ GET /learn/api/public/v2/courses/{courseId}/gradebook/columns/{id}/attempts
50
+ GET /learn/api/public/v1/courses/{courseId}/contents/{id}/attachments
51
+ GET /learn/api/public/v1/courses/{courseId}/contents/{id}/attachments/{id}/download
52
+ ```
53
+
54
+ ## MCP tools available
55
+
56
+ | Tool | What it does |
57
+ |------|-------------|
58
+ | `whoami` | Current student info |
59
+ | `system_version` | Server version |
60
+ | `list_courses` | All enrolled courses |
61
+ | `get_course` | Single course details |
62
+ | `list_contents` | Course materials tree |
63
+ | `list_announcements` | Course announcements |
64
+ | `list_assignments` | Tasks with due dates + grades |
65
+ | `list_attempts` | Submission history |
66
+ | `get_grades` | Full grade report for a course |
67
+ | `list_attachments` | Files in a content item |
68
+ | `download_attachment` | Download file (base64) |
69
+ | `submit_attempt` | Submit assignment (confirm first!) |
70
+ | `raw_api` | Any other Blackboard endpoint |
package/README.md ADDED
@@ -0,0 +1,222 @@
1
+ # blackboard-cli
2
+
3
+ > Accede a **UPC Aula Virtual** desde la terminal — sin abrir el navegador.
4
+
5
+ CLI no oficial para estudiantes de la UPC. Consulta cursos, descarga materiales, revisa tareas y entregas directamente desde la línea de comandos. También expone un **servidor MCP** para que Claude lo use como herramientas nativas.
6
+
7
+ ```bash
8
+ npx blackboard-upc login
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Instalación
14
+
15
+ ```bash
16
+ # Opción 1 — usar directamente con npx (sin instalar)
17
+ npx blackboard-upc login
18
+
19
+ # Opción 2 — instalar globalmente
20
+ npm install -g blackboard-upc
21
+ blackboard login
22
+
23
+ # Opción 3 — clonar el repo
24
+ git clone https://github.com/aleoroni/blackboard-cli
25
+ cd blackboard-cli
26
+ npm install
27
+ npx playwright install chromium
28
+ node run.js login
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Primeros pasos
34
+
35
+ ### 1. Login
36
+
37
+ ```bash
38
+ blackboard login
39
+ ```
40
+
41
+ Se abre una ventana del navegador con el login de Microsoft UPC. Inicia sesión con tu cuenta `u20XXXXXXX@upc.edu.pe` (incluye MFA si lo tienes). La ventana se cierra sola y la sesión queda guardada 8 horas.
42
+
43
+ ```
44
+ ██████ ██ █████ ██████ ██ ██ ██████ ██████ █████ ██████ ██████
45
+ ...
46
+ CLI no oficial para UPC Aula Virtual · Blackboard Learn
47
+
48
+ ✓ Sesión guardada — expira en 8 horas
49
+ Usuario: Alejandro Daniel Oroncoy Almeyda
50
+
51
+ ¿Qué puedo hacer ahora?
52
+
53
+ blackboard courses list ver tus cursos del ciclo
54
+ blackboard assignments list <id> ver tareas pendientes y notas
55
+ blackboard courses contents <id> explorar materiales
56
+ blackboard download-folder <id> <fid> descargar toda una carpeta
57
+ ```
58
+
59
+ ### 2. Ver cursos y tareas
60
+
61
+ ```bash
62
+ blackboard courses list
63
+
64
+ _529630_1 Fundamentos de Arquitectura de Software [Ultra]
65
+ _529580_1 Finanzas e Ingeniería Económica [Ultra]
66
+ _529533_1 Desarrollo de Soluciones IOT [Ultra]
67
+ _529760_1 Diseño de Experimentos de Ingeniería de Software [Ultra]
68
+
69
+ blackboard assignments list _529760_1
70
+
71
+ _13890556_1 Tarea 1 [manual]
72
+ Nota: sin entregar · Máx: 3 pts · Entrega: 05/03/2026 (vencida hace 23d)
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Comandos
78
+
79
+ ### Sesión
80
+ ```bash
81
+ blackboard login # autenticación Microsoft SSO
82
+ blackboard logout # borrar sesión
83
+ blackboard whoami # usuario activo y tiempo restante
84
+ blackboard status # versión del servidor Blackboard
85
+ ```
86
+
87
+ ### Cursos
88
+ ```bash
89
+ blackboard courses list
90
+ blackboard courses get <courseId>
91
+ blackboard courses contents <courseId>
92
+ blackboard courses contents <courseId> --parent <folderId> # navegar subcarpetas
93
+ blackboard courses contents <courseId> --type file|folder|assignment
94
+ blackboard courses announcements <courseId>
95
+ blackboard courses grades <courseId>
96
+ ```
97
+
98
+ ### Tareas
99
+ ```bash
100
+ blackboard assignments list <courseId> # tareas con nota y fecha
101
+ blackboard assignments list <courseId> --pending # solo pendientes
102
+ blackboard assignments attempts <courseId> <id> # historial de entregas
103
+ blackboard assignments submit <courseId> <id> -f tarea.pdf
104
+ blackboard assignments submit <courseId> <id> -t "Mi respuesta" -c "Comentario"
105
+ blackboard assignments submit <courseId> <id> -f borrador.pdf --draft
106
+ ```
107
+
108
+ ### Descargas
109
+ ```bash
110
+ blackboard download <courseId> <contentId> # archivo individual
111
+ blackboard download-folder <courseId> <folderId> -o ./dir/ # carpeta completa
112
+ blackboard download-folder <courseId> <folderId> --filter "parcial"
113
+ ```
114
+
115
+ ### API raw / scripting
116
+ ```bash
117
+ blackboard api GET /learn/api/public/v1/users/me
118
+ blackboard api GET /learn/api/public/v1/courses -q "limit=10"
119
+ blackboard endpoints # catálogo de todos los endpoints conocidos
120
+ blackboard endpoints --json # para pipelines
121
+ ```
122
+
123
+ Todos los comandos aceptan `--json`. Los spinners van a `stderr`, por lo que `--json 2>/dev/null` es JSON limpio.
124
+
125
+ ---
126
+
127
+ ## Uso con Claude (MCP)
128
+
129
+ `blackboard-cli` incluye un servidor MCP que Claude puede usar como herramientas nativas.
130
+
131
+ ### Claude Code
132
+
133
+ Añade a `.mcp.json` en tu proyecto:
134
+
135
+ ```json
136
+ {
137
+ "mcpServers": {
138
+ "blackboard": {
139
+ "command": "npx",
140
+ "args": ["blackboard-upc", "mcp"]
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ ### Claude Desktop
147
+
148
+ Edita `~/Library/Application Support/Claude/claude_desktop_config.json`:
149
+
150
+ ```json
151
+ {
152
+ "mcpServers": {
153
+ "blackboard": {
154
+ "command": "npx",
155
+ "args": ["blackboard-upc", "mcp"]
156
+ }
157
+ }
158
+ }
159
+ ```
160
+
161
+ > **Nota:** Si usas instalación global, reemplaza `npx blackboard-upc` por la ruta absoluta del binario (`which blackboard`).
162
+
163
+ ### Herramientas MCP disponibles
164
+
165
+ | Herramienta | Descripción |
166
+ |---|---|
167
+ | `whoami` | Info del estudiante autenticado |
168
+ | `list_courses` | Cursos inscritos |
169
+ | `get_course` | Detalle de un curso |
170
+ | `list_contents` | Árbol de materiales |
171
+ | `list_announcements` | Anuncios del curso |
172
+ | `list_assignments` | Tareas con fechas y notas |
173
+ | `list_attempts` | Historial de entregas |
174
+ | `get_grades` | Notas del ciclo |
175
+ | `list_attachments` | Archivos de un contenido |
176
+ | `download_attachment` | Descargar archivo (base64) |
177
+ | `submit_attempt` | Entregar tarea (pide confirmación) |
178
+ | `raw_api` | Cualquier endpoint de Blackboard |
179
+
180
+ Con Claude puedes hacer cosas como:
181
+
182
+ > *"¿Qué tareas tengo pendientes esta semana?"*
183
+ > *"Descárgame todos los exámenes del curso de Finanzas"*
184
+ > *"¿Cuál es mi nota actual en Arquitectura de Software?"*
185
+
186
+ ---
187
+
188
+ ## Cómo funciona la autenticación
189
+
190
+ UPC usa **SAML SSO → Microsoft Azure AD**. El CLI:
191
+ 1. Abre Chromium (Playwright) en la URL SAML de UPC
192
+ 2. Te muestra el login de Microsoft — tú ingresas tus credenciales
193
+ 3. Captura las cookies de sesión automáticamente al redirigir a `/ultra`
194
+ 4. Guarda todo en `~/.blackboard-cli/session.json` (permisos `600`)
195
+
196
+ La sesión dura **8 horas**. Después necesitas volver a hacer `login`.
197
+
198
+ ---
199
+
200
+ ## Stack
201
+
202
+ - **TypeScript** + `tsx` — sin build step
203
+ - **Playwright** — maneja el flujo SAML/SSO
204
+ - **Axios** — llamadas a la REST API con cookies de sesión
205
+ - **Commander.js** — framework CLI
206
+ - **MCP SDK** — servidor para Claude
207
+ - **Chalk** + **Ora** — output en la terminal
208
+
209
+ ---
210
+
211
+ ## Notas
212
+
213
+ - Probado con Blackboard Learn `v4000.10.0` (UPC, 2026).
214
+ - CLI **no oficial** — sin afiliación con UPC ni Blackboard Inc.
215
+ - Úsalo solo con tu propia cuenta. Respeta los TOS de UPC.
216
+ - Las cookies se guardan localmente. No se envían a servidores externos.
217
+
218
+ ---
219
+
220
+ ## Licencia
221
+
222
+ MIT
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "blackboard-upc",
3
+ "version": "1.0.0",
4
+ "description": "CLI no oficial para UPC Aula Virtual (Blackboard Learn) — acceso desde la terminal y MCP para Claude",
5
+ "main": "run.js",
6
+ "bin": {
7
+ "blackboard": "./run.js",
8
+ "blackboard-upc": "./run.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/index.ts",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "keywords": [
16
+ "blackboard",
17
+ "upc",
18
+ "aula-virtual",
19
+ "cli",
20
+ "mcp",
21
+ "claude",
22
+ "lms",
23
+ "peru"
24
+ ],
25
+ "author": "Alejandro Oroncoy",
26
+ "homepage": "https://github.com/aleoroni/blackboard-cli",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/aleoroni/blackboard-cli.git"
30
+ },
31
+ "license": "ISC",
32
+ "type": "commonjs",
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.28.0",
35
+ "@types/inquirer": "^9.0.9",
36
+ "@types/node": "^22.0.0",
37
+ "axios": "^1.7.0",
38
+ "chalk": "^5.3.0",
39
+ "commander": "^12.1.0",
40
+ "form-data": "^4.0.5",
41
+ "inquirer": "^10.1.0",
42
+ "ora": "^8.1.0",
43
+ "playwright": "^1.47.0",
44
+ "tsx": "^4.19.0",
45
+ "typescript": "^5.5.0",
46
+ "zod": "^4.3.6"
47
+ }
48
+ }
package/run.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // Run TypeScript directly without build step
3
+ const { spawnSync } = require('child_process');
4
+ const path = require('path');
5
+ const tsx = path.join(__dirname, 'node_modules', '.bin', 'tsx');
6
+ const entry = path.join(__dirname, 'src', 'index.ts');
7
+ const result = spawnSync(tsx, [entry, ...process.argv.slice(2)], { stdio: 'inherit' });
8
+ process.exit(result.status ?? 0);
@@ -0,0 +1,135 @@
1
+ import type { AxiosInstance } from 'axios';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import FormData from 'form-data';
5
+
6
+ export interface GradeColumn {
7
+ id: string;
8
+ name: string;
9
+ contentId?: string;
10
+ score?: { possible: number };
11
+ availability?: { available: string };
12
+ grading?: {
13
+ type: 'Attempts' | 'Manual' | 'Calculated';
14
+ due?: string;
15
+ attemptsAllowed?: number;
16
+ scoringModel?: string;
17
+ };
18
+ gradebookCategoryId?: string;
19
+ scoreProviderHandle?: string;
20
+ includeInCalculations?: boolean;
21
+ }
22
+
23
+ export interface Attempt {
24
+ id: string;
25
+ userId?: string;
26
+ status: string;
27
+ displayGrade?: { score?: number; text?: string };
28
+ score?: number;
29
+ text?: string;
30
+ studentComments?: string;
31
+ studentSubmission?: string;
32
+ created?: string;
33
+ modified?: string;
34
+ attemptDate?: string;
35
+ files?: Array<{ id: string; fileName: string; mimeType: string }>;
36
+ }
37
+
38
+ export interface SubmitAttemptBody {
39
+ studentComments?: string;
40
+ studentSubmission?: string;
41
+ fileUploadIds?: string[];
42
+ status?: string;
43
+ }
44
+
45
+ export async function listAssignments(
46
+ client: AxiosInstance,
47
+ courseId: string
48
+ ): Promise<GradeColumn[]> {
49
+ const r = await client.get(`/learn/api/public/v2/courses/${courseId}/gradebook/columns`, {
50
+ params: { limit: 100 },
51
+ });
52
+ // Only return Attempts and Manual columns (student-relevant)
53
+ return (r.data.results as GradeColumn[]).filter(
54
+ (c) => c.grading?.type === 'Attempts' || c.grading?.type === 'Manual'
55
+ );
56
+ }
57
+
58
+ export async function getAssignment(
59
+ client: AxiosInstance,
60
+ courseId: string,
61
+ columnId: string
62
+ ): Promise<GradeColumn> {
63
+ const r = await client.get(`/learn/api/public/v2/courses/${courseId}/gradebook/columns/${columnId}`);
64
+ return r.data;
65
+ }
66
+
67
+ export async function listAttempts(
68
+ client: AxiosInstance,
69
+ courseId: string,
70
+ columnId: string
71
+ ): Promise<Attempt[]> {
72
+ const r = await client.get(
73
+ `/learn/api/public/v2/courses/${courseId}/gradebook/columns/${columnId}/attempts`,
74
+ { params: { limit: 20 } }
75
+ );
76
+ return r.data.results;
77
+ }
78
+
79
+ export async function getAttempt(
80
+ client: AxiosInstance,
81
+ courseId: string,
82
+ columnId: string,
83
+ attemptId: string
84
+ ): Promise<Attempt> {
85
+ const r = await client.get(
86
+ `/learn/api/public/v2/courses/${courseId}/gradebook/columns/${columnId}/attempts/${attemptId}`
87
+ );
88
+ return r.data;
89
+ }
90
+
91
+ export async function uploadFile(
92
+ client: AxiosInstance,
93
+ filePath: string
94
+ ): Promise<string> {
95
+ const fileName = path.basename(filePath);
96
+ const fileBuffer = fs.readFileSync(filePath);
97
+
98
+ const form = new FormData();
99
+ form.append('file', fileBuffer, {
100
+ filename: fileName,
101
+ contentType: 'application/octet-stream',
102
+ });
103
+
104
+ const r = await client.post('/learn/api/public/v1/uploads', form, {
105
+ headers: {
106
+ ...form.getHeaders(),
107
+ },
108
+ });
109
+ return r.data.id as string;
110
+ }
111
+
112
+ export async function submitAttempt(
113
+ client: AxiosInstance,
114
+ courseId: string,
115
+ columnId: string,
116
+ body: SubmitAttemptBody
117
+ ): Promise<Attempt> {
118
+ const r = await client.post(
119
+ `/learn/api/public/v2/courses/${courseId}/gradebook/columns/${columnId}/attempts`,
120
+ { ...body, status: body.status ?? 'NeedsGrading' }
121
+ );
122
+ return r.data;
123
+ }
124
+
125
+ export async function getMyGrade(
126
+ client: AxiosInstance,
127
+ courseId: string,
128
+ columnId: string,
129
+ userId: string
130
+ ): Promise<any> {
131
+ const r = await client.get(
132
+ `/learn/api/public/v1/courses/${courseId}/gradebook/users/${userId}/columns/${columnId}`
133
+ );
134
+ return r.data;
135
+ }
@@ -0,0 +1,41 @@
1
+ import axios, { AxiosInstance, AxiosError } from 'axios';
2
+ import type { Session } from '../types/index.js';
3
+
4
+ const BASE_URL = 'https://aulavirtual.upc.edu.pe';
5
+
6
+ export function createClient(session: Session): AxiosInstance {
7
+ // Build cookie header string
8
+ const cookieStr = session.cookies
9
+ .filter((c) => {
10
+ const domain = c.domain.replace(/^\./, '');
11
+ return 'aulavirtual.upc.edu.pe'.endsWith(domain) || domain === 'aulavirtual.upc.edu.pe';
12
+ })
13
+ .map((c) => `${c.name}=${c.value}`)
14
+ .join('; ');
15
+
16
+ const client = axios.create({
17
+ baseURL: BASE_URL,
18
+ headers: {
19
+ Cookie: cookieStr,
20
+ Accept: 'application/json',
21
+ 'Content-Type': 'application/json',
22
+ ...(session.xsrfToken ? { 'X-Blackboard-XSRF': session.xsrfToken } : {}),
23
+ },
24
+ withCredentials: true,
25
+ });
26
+
27
+ // Intercept 401 to give a helpful message
28
+ client.interceptors.response.use(
29
+ (r) => r,
30
+ (err: AxiosError) => {
31
+ if (err.response?.status === 401) {
32
+ const e = new Error('Session expired. Run: blackboard login');
33
+ (e as any).code = 'SESSION_EXPIRED';
34
+ throw e;
35
+ }
36
+ throw err;
37
+ }
38
+ );
39
+
40
+ return client;
41
+ }
@@ -0,0 +1,110 @@
1
+ import type { AxiosInstance } from 'axios';
2
+ import type { Course, UserCourse, PaginatedResponse } from '../types/index.js';
3
+
4
+ export async function getMe(client: AxiosInstance): Promise<any> {
5
+ const r = await client.get('/learn/api/public/v1/users/me');
6
+ return r.data;
7
+ }
8
+
9
+ export async function getMyCourses(
10
+ client: AxiosInstance,
11
+ userId: string,
12
+ opts: { limit?: number; offset?: number } = {}
13
+ ): Promise<PaginatedResponse<UserCourse & { course?: Course }>> {
14
+ const params: Record<string, any> = { limit: opts.limit ?? 50 };
15
+ if (opts.offset) params.offset = opts.offset;
16
+
17
+ const r = await client.get(`/learn/api/public/v1/users/${userId}/courses`, { params });
18
+ const memberships: UserCourse[] = r.data.results;
19
+
20
+ // Resolve course names in parallel
21
+ const courseDetails = await Promise.allSettled(
22
+ memberships.map((m) => getCourse(client, m.courseId))
23
+ );
24
+
25
+ const results = memberships.map((m, i) => ({
26
+ ...m,
27
+ course: courseDetails[i].status === 'fulfilled' ? courseDetails[i].value : undefined,
28
+ }));
29
+
30
+ return { results, paging: r.data.paging };
31
+ }
32
+
33
+ export async function getCourse(client: AxiosInstance, courseId: string): Promise<Course> {
34
+ const r = await client.get(`/learn/api/public/v1/courses/${courseId}`);
35
+ return r.data;
36
+ }
37
+
38
+ export async function listCourses(
39
+ client: AxiosInstance,
40
+ opts: { limit?: number; offset?: number } = {}
41
+ ): Promise<PaginatedResponse<Course>> {
42
+ const r = await client.get('/learn/api/public/v1/courses', {
43
+ params: { limit: opts.limit ?? 50, offset: opts.offset ?? 0 },
44
+ });
45
+ return r.data;
46
+ }
47
+
48
+ export async function getCourseContents(
49
+ client: AxiosInstance,
50
+ courseId: string,
51
+ parentId?: string
52
+ ): Promise<PaginatedResponse<any>> {
53
+ const path = parentId
54
+ ? `/learn/api/public/v1/courses/${courseId}/contents/${parentId}/children`
55
+ : `/learn/api/public/v1/courses/${courseId}/contents`;
56
+ const r = await client.get(path, {
57
+ params: {
58
+ limit: 100,
59
+ fields: 'id,parentId,title,body,created,modified,position,hasChildren,launchInNewWindow,availability,contentHandler',
60
+ },
61
+ });
62
+ return r.data;
63
+ }
64
+
65
+ export async function getCourseAnnouncements(
66
+ client: AxiosInstance,
67
+ courseId: string
68
+ ): Promise<PaginatedResponse<any>> {
69
+ const r = await client.get(`/learn/api/public/v1/courses/${courseId}/announcements`, {
70
+ params: { limit: 20 },
71
+ });
72
+ return r.data;
73
+ }
74
+
75
+ export async function getGradeColumns(
76
+ client: AxiosInstance,
77
+ courseId: string
78
+ ): Promise<PaginatedResponse<any>> {
79
+ const r = await client.get(`/learn/api/public/v1/courses/${courseId}/gradebook/columns`, {
80
+ params: { limit: 50 },
81
+ });
82
+ return r.data;
83
+ }
84
+
85
+ export async function getGrades(
86
+ client: AxiosInstance,
87
+ courseId: string,
88
+ userId: string
89
+ ): Promise<PaginatedResponse<any>> {
90
+ const r = await client.get(
91
+ `/learn/api/public/v1/courses/${courseId}/gradebook/users/${userId}`,
92
+ { params: { limit: 50 } }
93
+ );
94
+ return r.data;
95
+ }
96
+
97
+ export async function getCourseMemberships(
98
+ client: AxiosInstance,
99
+ courseId: string
100
+ ): Promise<PaginatedResponse<any>> {
101
+ const r = await client.get(`/learn/api/public/v1/courses/${courseId}/users`, {
102
+ params: { limit: 100 },
103
+ });
104
+ return r.data;
105
+ }
106
+
107
+ export async function getSystemVersion(client: AxiosInstance): Promise<any> {
108
+ const r = await client.get('/learn/api/public/v1/system/version');
109
+ return r.data;
110
+ }