blackboard-upc 1.0.0 → 1.0.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/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to `blackboard-upc` will be documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.0.1] — 2026-03-30
8
+
9
+ ### Added
10
+ - `courses members <courseId>` — lista compañeros e instructor de un curso (con `--role` y `--json`)
11
+
12
+ ### Improved
13
+ - `courses list` — usa `expand=course` en una sola llamada en vez de 1+N (antes: 1 llamada por curso)
14
+ - `assignments list` — usa bulk grades (`/gradebook/users/{id}`) en paralelo con columns, eliminando N llamadas individuales
15
+ - `courses members` — usa `expand=user` para traer nombres en una sola llamada
16
+
17
+ ---
18
+
7
19
  ## [1.0.0] — 2026-03-30
8
20
 
9
21
  ### Added
@@ -53,7 +65,7 @@ All notable changes to `blackboard-upc` will be documented here.
53
65
 
54
66
  ## Roadmap
55
67
 
56
- - [ ] `npx` install sin clonar repo (publicación en npm)
68
+ - [x] `npx` install sin clonar repo (publicación en npm)
57
69
  - [ ] Refresh automático de sesión antes de expirar
58
70
  - [ ] Notificaciones de entregas próximas (`assignments due`)
59
71
  - [ ] Descarga de videos de grabaciones de clase
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blackboard-upc",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "CLI no oficial para UPC Aula Virtual (Blackboard Learn) — acceso desde la terminal y MCP para Claude",
5
5
  "main": "run.js",
6
6
  "bin": {
@@ -14,20 +14,9 @@ export async function getMyCourses(
14
14
  const params: Record<string, any> = { limit: opts.limit ?? 50 };
15
15
  if (opts.offset) params.offset = opts.offset;
16
16
 
17
+ params.expand = 'course';
17
18
  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 };
19
+ return { results: r.data.results, paging: r.data.paging };
31
20
  }
32
21
 
33
22
  export async function getCourse(client: AxiosInstance, courseId: string): Promise<Course> {
@@ -61,7 +61,16 @@ export function assignmentsCommand(program: Command) {
61
61
  let userId = session.userId;
62
62
  if (!userId) { const me = await getMe(client); userId = me.id; }
63
63
 
64
- const columns = await listAssignments(client, courseId);
64
+ const [columns, gradesRes] = await Promise.all([
65
+ listAssignments(client, courseId),
66
+ client
67
+ .get(`/learn/api/public/v1/courses/${courseId}/gradebook/users/${userId}`, {
68
+ params: { limit: 200 },
69
+ })
70
+ .then((r) => r.data.results as any[])
71
+ .catch(() => [] as any[]),
72
+ ]);
73
+
65
74
  spinner.succeed(`${columns.length} assignments found`);
66
75
 
67
76
  if (opts.json) { console.log(JSON.stringify(columns, null, 2)); return; }
@@ -71,19 +80,11 @@ export function assignmentsCommand(program: Command) {
71
80
  return;
72
81
  }
73
82
 
74
- // Fetch my grade for each assignment in parallel
75
- const gradeResults = await Promise.allSettled(
76
- columns.map((c) =>
77
- client
78
- .get(`/learn/api/public/v1/courses/${courseId}/gradebook/users/${userId}/columns/${c.id}`)
79
- .then((r) => r.data)
80
- .catch(() => null)
81
- )
82
- );
83
+ const gradeMap = new Map(gradesRes.map((g: any) => [g.columnId, g]));
83
84
 
84
85
  console.log('');
85
- columns.forEach((col, i) => {
86
- const grade = gradeResults[i].status === 'fulfilled' ? gradeResults[i].value : null;
86
+ columns.forEach((col) => {
87
+ const grade = gradeMap.get(col.id) ?? null;
87
88
  const possible = col.score?.possible ?? '?';
88
89
  const due = col.grading?.due;
89
90
  const attemptsAllowed = col.grading?.attemptsAllowed === 0
@@ -231,6 +231,63 @@ export function coursesCommand(program: Command) {
231
231
  }
232
232
  });
233
233
 
234
+ // Members
235
+ courses
236
+ .command('members <courseId>')
237
+ .description('List students and instructors in a course')
238
+ .option('--json', 'Output raw JSON')
239
+ .option('--role <role>', 'Filter by role: Student, Instructor')
240
+ .action(async (courseId, opts) => {
241
+ const session = requireSession();
242
+ const client = createClient(session);
243
+ const spinner = ora({ text: 'Fetching course members...', stream: process.stderr }).start();
244
+
245
+ try {
246
+ const res = await client.get(
247
+ `/learn/api/public/v1/courses/${courseId}/users?expand=user&limit=200`
248
+ );
249
+ let members: any[] = res.data.results ?? [];
250
+
251
+ if (opts.role) {
252
+ const roleFilter = opts.role.toLowerCase();
253
+ members = members.filter((m: any) => m.courseRoleId?.toLowerCase() === roleFilter);
254
+ }
255
+
256
+ spinner.succeed(`${members.length} members`);
257
+
258
+ if (opts.json) {
259
+ console.log(JSON.stringify({ results: members }, null, 2));
260
+ return;
261
+ }
262
+
263
+ const students = members.filter((m: any) => m.courseRoleId === 'Student');
264
+ const instructors = members.filter((m: any) => m.courseRoleId !== 'Student');
265
+
266
+ console.log('');
267
+ if (instructors.length > 0) {
268
+ console.log(chalk.bold(' Instructores'));
269
+ for (const m of instructors) {
270
+ const name = m.user ? `${m.user.name.given} ${m.user.name.family}` : m.userId;
271
+ console.log(` ${chalk.yellow(name)}`);
272
+ }
273
+ console.log('');
274
+ }
275
+
276
+ if (students.length > 0) {
277
+ console.log(chalk.bold(` Estudiantes (${students.length})`));
278
+ students.forEach((m: any, i: number) => {
279
+ const name = m.user ? `${m.user.name.given} ${m.user.name.family}` : m.userId;
280
+ const num = chalk.gray(`${String(i + 1).padStart(2, ' ')}.`);
281
+ console.log(` ${num} ${name}`);
282
+ });
283
+ }
284
+ console.log('');
285
+ } catch (err: any) {
286
+ spinner.fail(err.message);
287
+ process.exit(1);
288
+ }
289
+ });
290
+
234
291
  // Grades
235
292
  courses
236
293
  .command('grades <courseId>')