@wilm-ai/wilma-cli 0.0.11 → 1.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 (3) hide show
  1. package/README.md +42 -5
  2. package/dist/index.js +475 -24
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -16,11 +16,47 @@ wilma
16
16
  wilmai
17
17
  ```
18
18
 
19
- ## Non-interactive (agent-friendly)
19
+ ## Commands
20
+
21
+ ### Daily briefing
22
+ ```bash
23
+ wilma summary [--days 7] [--student <id|name>] [--all-students] [--json]
24
+ ```
25
+ Combines today's and tomorrow's schedule, upcoming exams, recent homework, news, and messages into one view. Designed for AI agents to surface what matters.
26
+
27
+ ### Schedule
28
+ ```bash
29
+ wilma schedule list [--when today|tomorrow|week] [--student <id|name>] [--all-students] [--json]
30
+ ```
31
+
32
+ ### Homework
33
+ ```bash
34
+ wilma homework list [--limit 10] [--student <id|name>] [--all-students] [--json]
35
+ ```
36
+
37
+ ### Upcoming exams
38
+ ```bash
39
+ wilma exams list [--limit 20] [--student <id|name>] [--all-students] [--json]
40
+ ```
41
+
42
+ ### Exam grades
43
+ ```bash
44
+ wilma grades list [--limit 20] [--student <id|name>] [--all-students] [--json]
45
+ ```
46
+
47
+ ### News and messages
48
+ ```bash
49
+ wilma news list [--limit 20] [--student <id|name>] [--all-students] [--json]
50
+ wilma news read <id> [--student <id|name>] [--json]
51
+ wilma messages list [--folder inbox] [--limit 20] [--student <id|name>] [--all-students] [--json]
52
+ wilma messages read <id> [--student <id|name>] [--json]
53
+ ```
54
+
55
+ ### Other
20
56
  ```bash
21
- wilma kids list --json
22
- wilma news list --all --json
23
- wilma messages list --folder inbox --all --json
57
+ wilma kids list [--json]
58
+ wilma update
59
+ wilma config clear
24
60
  ```
25
61
 
26
62
  ## Config
@@ -29,4 +65,5 @@ Use `wilma config clear` to remove it. Override with `WILMAI_CONFIG_PATH`.
29
65
 
30
66
  ## Notes
31
67
  - Credentials are stored with lightweight obfuscation for convenience.
32
- - For multi-child accounts, you can pass `--student <id>` or `--all`.
68
+ - For multi-child accounts, you can pass `--student <id|name>` or `--all-students`.
69
+ - All list commands support `--json` for agent-friendly structured output.
package/dist/index.js CHANGED
@@ -11,8 +11,13 @@ if (process.stdin.isTTY) {
11
11
  emitKeypressEvents(process.stdin);
12
12
  }
13
13
  const ACTIONS = [
14
+ { value: "summary", name: "Daily summary" },
15
+ { value: "schedule-today", name: "Today's schedule" },
16
+ { value: "schedule-tomorrow", name: "Tomorrow's schedule" },
17
+ { value: "homework", name: "Recent homework" },
18
+ { value: "exams", name: "Upcoming exams" },
19
+ { value: "grades", name: "Exam grades" },
14
20
  { value: "news", name: "List news" },
15
- { value: "exams", name: "List exams" },
16
21
  { value: "messages", name: "List messages" },
17
22
  { value: "exit", name: "Exit" },
18
23
  ];
@@ -140,6 +145,7 @@ async function runInteractive(config) {
140
145
  const client = await WilmaClient.login(profile);
141
146
  let nextAction = await selectOrCancel({
142
147
  message: "What do you want to view?",
148
+ pageSize: 15,
143
149
  choices: [
144
150
  ...ACTIONS.filter((a) => a.value !== "exit"),
145
151
  { value: "back", name: "Back to students" },
@@ -151,12 +157,32 @@ async function runInteractive(config) {
151
157
  continue;
152
158
  }
153
159
  while (nextAction !== "exit" && nextAction !== "back") {
154
- if (nextAction === "news") {
155
- await selectNewsToRead(client);
160
+ if (nextAction === "summary") {
161
+ console.clear();
162
+ await outputSummary(client, { days: 7, json: false });
163
+ }
164
+ if (nextAction === "schedule-today") {
165
+ console.clear();
166
+ await outputSchedule(client, { when: "today", json: false });
167
+ }
168
+ if (nextAction === "schedule-tomorrow") {
169
+ console.clear();
170
+ await outputSchedule(client, { when: "tomorrow", json: false });
171
+ }
172
+ if (nextAction === "homework") {
173
+ console.clear();
174
+ await outputHomework(client, { limit: 10, json: false });
156
175
  }
157
176
  if (nextAction === "exams") {
158
177
  console.clear();
159
- await outputExams(client, { limit: 20, json: false });
178
+ await outputUpcomingExams(client, { limit: 20, json: false });
179
+ }
180
+ if (nextAction === "grades") {
181
+ console.clear();
182
+ await outputGrades(client, { limit: 20, json: false });
183
+ }
184
+ if (nextAction === "news") {
185
+ await selectNewsToRead(client);
160
186
  }
161
187
  if (nextAction === "messages") {
162
188
  const folder = await selectOrCancel({
@@ -175,6 +201,7 @@ async function runInteractive(config) {
175
201
  }
176
202
  nextAction = await selectOrCancel({
177
203
  message: "What next?",
204
+ pageSize: 15,
178
205
  choices: [
179
206
  ...ACTIONS.filter((a) => a.value !== "exit"),
180
207
  { value: "back", name: "Back to students" },
@@ -365,6 +392,7 @@ async function handleCommand(args, config) {
365
392
  }
366
393
  if (command === "news") {
367
394
  if (subcommand === "read" && flags.id) {
395
+ const newsId = parseReadId(flags.id, "news");
368
396
  if (!flags.student) {
369
397
  const students = await getStudentsForCommand(profile, config);
370
398
  if (students.length > 1) {
@@ -382,7 +410,7 @@ async function handleCommand(args, config) {
382
410
  ...profile,
383
411
  studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
384
412
  });
385
- await outputNewsItem(perStudentClient, Number(flags.id), flags.json);
413
+ await outputNewsItem(perStudentClient, newsId, flags.json);
386
414
  return;
387
415
  }
388
416
  if (flags.allStudents) {
@@ -407,6 +435,7 @@ async function handleCommand(args, config) {
407
435
  }
408
436
  if (command === "messages") {
409
437
  if (subcommand === "read" && flags.id) {
438
+ const messageId = parseReadId(flags.id, "message");
410
439
  if (!flags.student) {
411
440
  const students = await getStudentsForCommand(profile, config);
412
441
  if (students.length > 1) {
@@ -424,7 +453,7 @@ async function handleCommand(args, config) {
424
453
  ...profile,
425
454
  studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
426
455
  });
427
- await outputMessageItem(perStudentClient, Number(flags.id), flags.json);
456
+ await outputMessageItem(perStudentClient, messageId, flags.json);
428
457
  return;
429
458
  }
430
459
  if (flags.allStudents) {
@@ -466,33 +495,111 @@ async function handleCommand(args, config) {
466
495
  ...profile,
467
496
  studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
468
497
  });
469
- await outputExams(perStudentClient, {
498
+ await outputUpcomingExams(perStudentClient, {
470
499
  limit: flags.limit ?? 20,
471
500
  json: flags.json,
472
501
  label: studentInfo?.name ?? undefined,
473
502
  });
474
503
  return;
475
504
  }
476
- console.log("Usage:");
477
- console.log(" wilma kids list [--json]");
478
- console.log(" wilma news list [--limit 20] [--student <id|name>] [--all-students] [--json]");
479
- console.log(" wilma news read <id> [--student <id|name>] [--json]");
480
- console.log(" wilma messages list [--folder inbox] [--limit 20] [--student <id|name>] [--all-students] [--json]");
481
- console.log(" wilma messages read <id> [--student <id|name>] [--json]");
482
- console.log(" wilma exams list [--limit 20] [--student <id|name>] [--all-students] [--json]");
483
- console.log(" wilma update");
484
- console.log(" wilma config clear");
485
- console.log(" wilma --help | -h");
486
- console.log(" wilma --version | -v");
505
+ if (command === "schedule") {
506
+ if (flags.allStudents) {
507
+ await outputAllOverviewCommand(profile, config, "schedule", flags);
508
+ return;
509
+ }
510
+ const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
511
+ if (!studentInfo && !profile.studentNumber) {
512
+ await printStudentSelectionHelp(profile, config);
513
+ return;
514
+ }
515
+ const perStudentClient = await WilmaClient.login({
516
+ ...profile,
517
+ studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
518
+ });
519
+ await outputSchedule(perStudentClient, {
520
+ when: flags.when ?? "week",
521
+ json: flags.json,
522
+ label: studentInfo?.name ?? undefined,
523
+ });
524
+ return;
525
+ }
526
+ if (command === "homework") {
527
+ if (flags.allStudents) {
528
+ await outputAllOverviewCommand(profile, config, "homework", flags);
529
+ return;
530
+ }
531
+ const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
532
+ if (!studentInfo && !profile.studentNumber) {
533
+ await printStudentSelectionHelp(profile, config);
534
+ return;
535
+ }
536
+ const perStudentClient = await WilmaClient.login({
537
+ ...profile,
538
+ studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
539
+ });
540
+ await outputHomework(perStudentClient, {
541
+ limit: flags.limit ?? 10,
542
+ json: flags.json,
543
+ label: studentInfo?.name ?? undefined,
544
+ });
545
+ return;
546
+ }
547
+ if (command === "grades") {
548
+ if (flags.allStudents) {
549
+ await outputAllOverviewCommand(profile, config, "grades", flags);
550
+ return;
551
+ }
552
+ const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
553
+ if (!studentInfo && !profile.studentNumber) {
554
+ await printStudentSelectionHelp(profile, config);
555
+ return;
556
+ }
557
+ const perStudentClient = await WilmaClient.login({
558
+ ...profile,
559
+ studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
560
+ });
561
+ await outputGrades(perStudentClient, {
562
+ limit: flags.limit ?? 20,
563
+ json: flags.json,
564
+ label: studentInfo?.name ?? undefined,
565
+ });
566
+ return;
567
+ }
568
+ if (command === "summary") {
569
+ if (flags.allStudents) {
570
+ await outputAllOverviewCommand(profile, config, "summary", flags);
571
+ return;
572
+ }
573
+ const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
574
+ if (!studentInfo && !profile.studentNumber) {
575
+ await printStudentSelectionHelp(profile, config);
576
+ return;
577
+ }
578
+ const perStudentClient = await WilmaClient.login({
579
+ ...profile,
580
+ studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
581
+ });
582
+ await outputSummary(perStudentClient, {
583
+ days: flags.days ?? 7,
584
+ json: flags.json,
585
+ label: studentInfo?.name ?? undefined,
586
+ });
587
+ return;
588
+ }
589
+ printUsage();
487
590
  }
488
591
  function printUsage() {
489
592
  console.log("Usage:");
593
+ console.log(" wilma summary [--days 7] [--student <id|name>] [--all-students] [--json]");
594
+ console.log(" wilma schedule list [--when today|tomorrow|week] [--student <id|name>] [--all-students] [--json]");
595
+ console.log(" wilma homework list [--limit 10] [--student <id|name>] [--all-students] [--json]");
596
+ console.log(" wilma exams list [--limit 20] [--student <id|name>] [--all-students] [--json]");
597
+ console.log(" wilma grades list [--limit 20] [--student <id|name>] [--all-students] [--json]");
490
598
  console.log(" wilma kids list [--json]");
491
599
  console.log(" wilma news list [--limit 20] [--student <id|name>] [--all-students] [--json]");
492
600
  console.log(" wilma news read <id> [--student <id|name>] [--json]");
493
601
  console.log(" wilma messages list [--folder inbox] [--limit 20] [--student <id|name>] [--all-students] [--json]");
494
602
  console.log(" wilma messages read <id> [--student <id|name>] [--json]");
495
- console.log(" wilma exams list [--limit 20] [--student <id|name>] [--all-students] [--json]");
496
603
  console.log(" wilma update");
497
604
  console.log(" wilma config clear");
498
605
  console.log(" wilma --help | -h");
@@ -522,7 +629,10 @@ async function getProfileForCommandNonInteractive(config, flags) {
522
629
  };
523
630
  }
524
631
  function parseArgs(args) {
525
- const [command, subcommand, ...rest] = args;
632
+ const [command, rawSubcommand, ...rawRest] = args;
633
+ // If "subcommand" is actually a flag, push it back into rest
634
+ const subcommand = rawSubcommand?.startsWith("--") ? undefined : rawSubcommand;
635
+ const rest = rawSubcommand?.startsWith("--") ? [rawSubcommand, ...rawRest] : rawRest;
526
636
  const flags = {};
527
637
  let i = 0;
528
638
  while (i < rest.length) {
@@ -560,6 +670,19 @@ function parseArgs(args) {
560
670
  i += 2;
561
671
  continue;
562
672
  }
673
+ if (arg === "--when") {
674
+ flags.when = rest[i + 1];
675
+ i += 2;
676
+ continue;
677
+ }
678
+ if (arg === "--days") {
679
+ const value = Number(rest[i + 1]);
680
+ if (!Number.isNaN(value)) {
681
+ flags.days = value;
682
+ }
683
+ i += 2;
684
+ continue;
685
+ }
563
686
  if (!flags.id && !arg.startsWith("--")) {
564
687
  flags.id = arg;
565
688
  i += 1;
@@ -569,6 +692,18 @@ function parseArgs(args) {
569
692
  }
570
693
  return { command, subcommand, flags };
571
694
  }
695
+ function parseReadId(raw, entity) {
696
+ if (!raw) {
697
+ console.error(`Missing ${entity} id.`);
698
+ process.exit(1);
699
+ }
700
+ const id = Number(raw);
701
+ if (!Number.isInteger(id) || id <= 0) {
702
+ console.error(`Invalid ${entity} id "${raw}". Expected a positive integer.`);
703
+ process.exit(1);
704
+ }
705
+ return id;
706
+ }
572
707
  async function readPackageVersion() {
573
708
  const pkgPath = resolve(dirname(new URL(import.meta.url).pathname), "..", "package.json");
574
709
  const raw = await readFile(pkgPath, "utf-8");
@@ -726,6 +861,228 @@ async function outputExams(client, opts) {
726
861
  console.log(`- ${prefix}${date} ${compactText(exam.subject)}`);
727
862
  });
728
863
  }
864
+ /* ------------------------------------------------------------------ */
865
+ /* Overview-powered output functions */
866
+ /* ------------------------------------------------------------------ */
867
+ function todayString() {
868
+ const d = new Date();
869
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
870
+ }
871
+ function nextSchoolDay(from) {
872
+ const d = from ? new Date(from + "T12:00:00") : new Date();
873
+ d.setDate(d.getDate() + 1);
874
+ // Skip Saturday (6) and Sunday (0)
875
+ while (d.getDay() === 0 || d.getDay() === 6) {
876
+ d.setDate(d.getDate() + 1);
877
+ }
878
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
879
+ }
880
+ function currentWeekBounds() {
881
+ const now = new Date();
882
+ const dayOfWeek = now.getDay(); // 0=Sun, 1=Mon, ...
883
+ const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
884
+ const monday = new Date(now);
885
+ monday.setDate(now.getDate() + diffToMonday);
886
+ const friday = new Date(monday);
887
+ friday.setDate(monday.getDate() + 4);
888
+ const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
889
+ return [fmt(monday), fmt(friday)];
890
+ }
891
+ const DAY_NAMES = ["Su", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
892
+ async function outputSchedule(client, opts) {
893
+ const overview = await client.overview.get();
894
+ const when = opts.when || "week";
895
+ let startDate;
896
+ let endDate;
897
+ if (when === "today") {
898
+ startDate = endDate = todayString();
899
+ }
900
+ else if (when === "tomorrow") {
901
+ startDate = endDate = nextSchoolDay();
902
+ }
903
+ else {
904
+ [startDate, endDate] = currentWeekBounds();
905
+ }
906
+ const lessons = overview.schedule.filter((l) => l.date >= startDate && l.date <= endDate);
907
+ if (opts.json) {
908
+ const result = when === "week"
909
+ ? { when, weekStart: startDate, weekEnd: endDate, lessons }
910
+ : { when, date: startDate, lessons };
911
+ console.log(JSON.stringify(result, null, 2));
912
+ return;
913
+ }
914
+ const prefix = opts.label ? `[${opts.label}] ` : "";
915
+ if (when === "today") {
916
+ console.log(`\n${prefix}Schedule for today (${startDate})`);
917
+ }
918
+ else if (when === "tomorrow") {
919
+ console.log(`\n${prefix}Schedule for tomorrow (${startDate})`);
920
+ }
921
+ else {
922
+ console.log(`\n${prefix}Schedule for ${startDate} – ${endDate}`);
923
+ }
924
+ if (!lessons.length) {
925
+ console.log(" No lessons found.");
926
+ return;
927
+ }
928
+ let currentDate = "";
929
+ for (const l of lessons) {
930
+ if (l.date !== currentDate) {
931
+ currentDate = l.date;
932
+ const d = new Date(l.date + "T12:00:00");
933
+ console.log(` ${DAY_NAMES[d.getDay()]} ${l.date}`);
934
+ }
935
+ const teacher = l.teacherCode ? ` - ${l.teacher}` : "";
936
+ console.log(` ${l.start}-${l.end} ${l.subject}${teacher}`);
937
+ }
938
+ }
939
+ async function outputHomework(client, opts) {
940
+ const overview = await client.overview.get();
941
+ const slice = overview.homework.slice(0, opts.limit);
942
+ if (opts.json) {
943
+ console.log(JSON.stringify(slice, null, 2));
944
+ return;
945
+ }
946
+ const prefix = opts.label ? `[${opts.label}] ` : "";
947
+ console.log(`\n${prefix}Homework (${overview.homework.length})`);
948
+ slice.forEach((hw) => {
949
+ const text = compactText(hw.homework);
950
+ console.log(`- ${hw.date} ${hw.subject}: ${text}`);
951
+ });
952
+ }
953
+ async function outputUpcomingExams(client, opts) {
954
+ const overview = await client.overview.get();
955
+ const slice = overview.upcomingExams.slice(0, opts.limit);
956
+ if (opts.json) {
957
+ console.log(JSON.stringify(slice, null, 2));
958
+ return;
959
+ }
960
+ const prefix = opts.label ? `[${opts.label}] ` : "";
961
+ console.log(`\n${prefix}Upcoming exams (${overview.upcomingExams.length})`);
962
+ slice.forEach((exam) => {
963
+ const topic = exam.topic ? ` — ${compactText(exam.topic)}` : "";
964
+ console.log(`- ${exam.date} ${exam.subject}: ${exam.name}${topic}`);
965
+ });
966
+ }
967
+ async function outputGrades(client, opts) {
968
+ const overview = await client.overview.get();
969
+ const slice = overview.grades.slice(0, opts.limit);
970
+ if (opts.json) {
971
+ console.log(JSON.stringify(slice, null, 2));
972
+ return;
973
+ }
974
+ const prefix = opts.label ? `[${opts.label}] ` : "";
975
+ console.log(`\n${prefix}Grades (${overview.grades.length})`);
976
+ slice.forEach((g) => {
977
+ console.log(`- ${g.date} ${g.subject}: ${g.name} — ${g.grade}`);
978
+ });
979
+ }
980
+ async function outputSummary(client, opts) {
981
+ const [overview, news, messages] = await Promise.all([
982
+ client.overview.get(),
983
+ client.news.list(),
984
+ client.messages.list("inbox"),
985
+ ]);
986
+ const summary = buildSummaryData(overview, news, messages, opts.days, opts.label);
987
+ if (opts.json) {
988
+ console.log(JSON.stringify(summary, null, 2));
989
+ return;
990
+ }
991
+ const label = summary.student ? ` for ${summary.student}` : "";
992
+ console.log(`\nSummary${label} (${summary.today})`);
993
+ console.log(`\nTODAY (${summary.today})`);
994
+ if (summary.todaySchedule.length) {
995
+ summary.todaySchedule.forEach((l) => {
996
+ console.log(` ${l.start}-${l.end} ${l.subject} (${l.subjectCode})`);
997
+ });
998
+ }
999
+ else {
1000
+ console.log(" No lessons today.");
1001
+ }
1002
+ console.log(`\nTOMORROW (${summary.tomorrow})`);
1003
+ if (summary.tomorrowSchedule.length) {
1004
+ summary.tomorrowSchedule.forEach((l) => {
1005
+ console.log(` ${l.start}-${l.end} ${l.subject} (${l.subjectCode})`);
1006
+ });
1007
+ }
1008
+ else {
1009
+ console.log(" No lessons tomorrow.");
1010
+ }
1011
+ if (summary.upcomingExams.length) {
1012
+ console.log("\nUPCOMING EXAMS");
1013
+ summary.upcomingExams.forEach((exam) => {
1014
+ const topic = exam.topic ? ` — ${compactText(exam.topic)}` : "";
1015
+ console.log(` ${exam.date} ${exam.subject}: ${exam.name}${topic}`);
1016
+ });
1017
+ }
1018
+ if (summary.recentHomework.length) {
1019
+ console.log("\nRECENT HOMEWORK");
1020
+ summary.recentHomework.forEach((hw) => {
1021
+ console.log(` ${hw.date} ${hw.subject}: ${compactText(hw.homework)}`);
1022
+ });
1023
+ }
1024
+ if (summary.recentNews.length) {
1025
+ console.log(`\nNEWS (last ${opts.days} days)`);
1026
+ summary.recentNews.forEach((n) => {
1027
+ const date = n.published ? n.published.slice(0, 10) : "";
1028
+ console.log(` ${date} ${compactText(n.title)} (id:${n.wilmaId})`);
1029
+ });
1030
+ }
1031
+ if (summary.recentMessages.length) {
1032
+ console.log(`\nMESSAGES (last ${opts.days} days)`);
1033
+ summary.recentMessages.forEach((m) => {
1034
+ const date = m.sentAt.slice(0, 10);
1035
+ console.log(` ${date} ${compactText(m.subject)} (id:${m.wilmaId})`);
1036
+ });
1037
+ }
1038
+ }
1039
+ function buildSummaryData(overview, news, messages, days, studentLabel) {
1040
+ const today = todayString();
1041
+ const tomorrow = nextSchoolDay();
1042
+ const cutoffDate = (() => {
1043
+ const d = new Date();
1044
+ d.setDate(d.getDate() - days);
1045
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1046
+ })();
1047
+ const homeworkCutoff = (() => {
1048
+ const d = new Date();
1049
+ d.setDate(d.getDate() - 3);
1050
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1051
+ })();
1052
+ const todaySchedule = overview.schedule.filter((l) => l.date === today);
1053
+ const tomorrowSchedule = overview.schedule.filter((l) => l.date === tomorrow);
1054
+ const upcomingExams = overview.upcomingExams;
1055
+ const recentHomework = overview.homework.filter((h) => h.date >= homeworkCutoff);
1056
+ const recentNews = news
1057
+ .filter((n) => n.published && n.published.toISOString().slice(0, 10) >= cutoffDate)
1058
+ .slice(0, 5)
1059
+ .map((n) => ({
1060
+ wilmaId: n.wilmaId,
1061
+ title: n.title,
1062
+ published: n.published?.toISOString() ?? null,
1063
+ }));
1064
+ const recentMessages = messages
1065
+ .filter((m) => m.sentAt.toISOString().slice(0, 10) >= cutoffDate)
1066
+ .slice(0, 5)
1067
+ .map((m) => ({
1068
+ wilmaId: m.wilmaId,
1069
+ subject: m.subject,
1070
+ sentAt: m.sentAt.toISOString(),
1071
+ senderName: m.senderName ?? null,
1072
+ }));
1073
+ return {
1074
+ generatedAt: new Date().toISOString(),
1075
+ student: studentLabel ?? null,
1076
+ today,
1077
+ tomorrow,
1078
+ todaySchedule,
1079
+ tomorrowSchedule,
1080
+ upcomingExams,
1081
+ recentHomework,
1082
+ recentNews,
1083
+ recentMessages,
1084
+ };
1085
+ }
729
1086
  async function outputMessages(client, opts) {
730
1087
  const messages = await client.messages.list(opts.folder);
731
1088
  const slice = messages.slice(0, opts.limit);
@@ -909,6 +1266,10 @@ async function resolveStudentForFlags(profile, config, student) {
909
1266
  const exact = students.find((s) => s.studentNumber === student);
910
1267
  if (exact)
911
1268
  return exact;
1269
+ const needle = student.toLowerCase();
1270
+ const substring = students.find((s) => s.name.toLowerCase().includes(needle));
1271
+ if (substring)
1272
+ return substring;
912
1273
  const match = students.find((s) => fuzzyIncludes(s.name, student));
913
1274
  if (match)
914
1275
  return match;
@@ -976,8 +1337,8 @@ async function outputAllExams(profile, config, limit, json) {
976
1337
  const results = [];
977
1338
  for (const student of students) {
978
1339
  const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
979
- const exams = await client.exams.list();
980
- results.push({ student, items: exams.slice(0, limit) });
1340
+ const overview = await client.overview.get();
1341
+ results.push({ student, items: overview.upcomingExams.slice(0, limit) });
981
1342
  }
982
1343
  if (json) {
983
1344
  console.log(JSON.stringify({ students: results }, null, 2));
@@ -986,11 +1347,101 @@ async function outputAllExams(profile, config, limit, json) {
986
1347
  results.forEach((entry) => {
987
1348
  console.log(`\n[${entry.student.name}]`);
988
1349
  entry.items.forEach((exam) => {
989
- const date = exam.examDate.toISOString().slice(0, 10);
990
- console.log(`- ${date} ${exam.subject}`);
1350
+ const topic = exam.topic ? ` — ${compactText(exam.topic)}` : "";
1351
+ console.log(`- ${exam.date} ${exam.subject}: ${exam.name}${topic}`);
991
1352
  });
992
1353
  });
993
1354
  }
1355
+ async function outputAllOverviewCommand(profile, config, command, flags) {
1356
+ const students = await getStudentsForCommand(profile, config);
1357
+ if (command === "summary") {
1358
+ if (flags.json) {
1359
+ const summaries = [];
1360
+ for (const student of students) {
1361
+ const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
1362
+ const [overview, news, messages] = await Promise.all([
1363
+ client.overview.get(),
1364
+ client.news.list(),
1365
+ client.messages.list("inbox"),
1366
+ ]);
1367
+ summaries.push({
1368
+ student,
1369
+ summary: buildSummaryData(overview, news, messages, flags.days ?? 7, student.name),
1370
+ });
1371
+ }
1372
+ console.log(JSON.stringify({
1373
+ generatedAt: new Date().toISOString(),
1374
+ students: summaries,
1375
+ }, null, 2));
1376
+ return;
1377
+ }
1378
+ // Human-readable summary output per student
1379
+ for (const student of students) {
1380
+ const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
1381
+ await outputSummary(client, {
1382
+ days: flags.days ?? 7,
1383
+ json: false,
1384
+ label: student.name,
1385
+ });
1386
+ }
1387
+ return;
1388
+ }
1389
+ const results = [];
1390
+ for (const student of students) {
1391
+ const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
1392
+ const overview = await client.overview.get();
1393
+ if (command === "schedule") {
1394
+ const when = flags.when || "week";
1395
+ let startDate, endDate;
1396
+ if (when === "today") {
1397
+ startDate = endDate = todayString();
1398
+ }
1399
+ else if (when === "tomorrow") {
1400
+ startDate = endDate = nextSchoolDay();
1401
+ }
1402
+ else {
1403
+ [startDate, endDate] = currentWeekBounds();
1404
+ }
1405
+ results.push({
1406
+ student,
1407
+ data: overview.schedule.filter((l) => l.date >= startDate && l.date <= endDate),
1408
+ });
1409
+ }
1410
+ else if (command === "homework") {
1411
+ results.push({ student, data: overview.homework.slice(0, flags.limit ?? 10) });
1412
+ }
1413
+ else if (command === "grades") {
1414
+ results.push({ student, data: overview.grades.slice(0, flags.limit ?? 20) });
1415
+ }
1416
+ }
1417
+ if (flags.json) {
1418
+ console.log(JSON.stringify({ students: results.map((r) => ({ student: r.student, items: r.data })) }, null, 2));
1419
+ return;
1420
+ }
1421
+ for (const r of results) {
1422
+ console.log(`\n[${r.student.name}]`);
1423
+ const items = r.data;
1424
+ if (!items.length) {
1425
+ console.log(" (none)");
1426
+ continue;
1427
+ }
1428
+ if (command === "schedule") {
1429
+ for (const l of items) {
1430
+ console.log(` ${l.date} ${l.start}-${l.end} ${l.subject} - ${l.teacher}`);
1431
+ }
1432
+ }
1433
+ else if (command === "homework") {
1434
+ for (const hw of items) {
1435
+ console.log(` ${hw.date} ${hw.subject}: ${compactText(hw.homework)}`);
1436
+ }
1437
+ }
1438
+ else if (command === "grades") {
1439
+ for (const g of items) {
1440
+ console.log(` ${g.date} ${g.subject}: ${g.name} — ${g.grade}`);
1441
+ }
1442
+ }
1443
+ }
1444
+ }
994
1445
  async function printStudentSelectionHelp(profile, config) {
995
1446
  const students = await getStudentsForCommand(profile, config);
996
1447
  console.error("Multiple students found. Use --student <id|name> or --all-students.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wilm-ai/wilma-cli",
3
- "version": "0.0.11",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@inquirer/prompts": "^5.3.8",
13
- "@wilm-ai/wilma-client": "^0.0.4"
13
+ "@wilm-ai/wilma-client": "^1.1.0"
14
14
  },
15
15
  "devDependencies": {
16
16
  "typescript": "^5.6.3"