@wilm-ai/wilma-cli 0.0.11 → 1.2.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 (4) hide show
  1. package/README.md +54 -5
  2. package/dist/index.js +567 -24
  3. package/package.json +11 -10
  4. package/LICENSE +0 -21
package/README.md CHANGED
@@ -16,11 +16,59 @@ 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] [--date YYYY-MM-DD] [--weekday mon|tue|wed|thu|fri|sat|sun] [--student <id|name>] [--all-students] [--json]
30
+ ```
31
+
32
+ Examples:
33
+ ```bash
34
+ # Specific day by date
35
+ wilma schedule list --date 2026-02-25 --student "Stella" --json
36
+
37
+ # Next Thursday (also accepts Finnish short forms like to/ke/pe)
38
+ wilma schedule list --weekday thu --student "Stella" --json
39
+
40
+ # Tomorrow
41
+ wilma schedule list --when tomorrow --student "Stella" --json
42
+ ```
43
+
44
+ ### Homework
45
+ ```bash
46
+ wilma homework list [--limit 10] [--student <id|name>] [--all-students] [--json]
47
+ ```
48
+
49
+ ### Upcoming exams
50
+ ```bash
51
+ wilma exams list [--limit 20] [--student <id|name>] [--all-students] [--json]
52
+ ```
53
+
54
+ ### Exam grades
55
+ ```bash
56
+ wilma grades list [--limit 20] [--student <id|name>] [--all-students] [--json]
57
+ ```
58
+
59
+ ### News and messages
60
+ ```bash
61
+ wilma news list [--limit 20] [--student <id|name>] [--all-students] [--json]
62
+ wilma news read <id> [--student <id|name>] [--json]
63
+ wilma messages list [--folder inbox] [--limit 20] [--student <id|name>] [--all-students] [--json]
64
+ wilma messages read <id> [--student <id|name>] [--json]
65
+ ```
66
+
67
+ ### Other
20
68
  ```bash
21
- wilma kids list --json
22
- wilma news list --all --json
23
- wilma messages list --folder inbox --all --json
69
+ wilma kids list [--json]
70
+ wilma update
71
+ wilma config clear
24
72
  ```
25
73
 
26
74
  ## Config
@@ -29,4 +77,5 @@ Use `wilma config clear` to remove it. Override with `WILMAI_CONFIG_PATH`.
29
77
 
30
78
  ## Notes
31
79
  - Credentials are stored with lightweight obfuscation for convenience.
32
- - For multi-child accounts, you can pass `--student <id>` or `--all`.
80
+ - For multi-child accounts, you can pass `--student <id|name>` or `--all-students`.
81
+ - 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,113 @@ 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
+ date: flags.date,
522
+ weekday: flags.weekday,
523
+ json: flags.json,
524
+ label: studentInfo?.name ?? undefined,
525
+ });
526
+ return;
527
+ }
528
+ if (command === "homework") {
529
+ if (flags.allStudents) {
530
+ await outputAllOverviewCommand(profile, config, "homework", flags);
531
+ return;
532
+ }
533
+ const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
534
+ if (!studentInfo && !profile.studentNumber) {
535
+ await printStudentSelectionHelp(profile, config);
536
+ return;
537
+ }
538
+ const perStudentClient = await WilmaClient.login({
539
+ ...profile,
540
+ studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
541
+ });
542
+ await outputHomework(perStudentClient, {
543
+ limit: flags.limit ?? 10,
544
+ json: flags.json,
545
+ label: studentInfo?.name ?? undefined,
546
+ });
547
+ return;
548
+ }
549
+ if (command === "grades") {
550
+ if (flags.allStudents) {
551
+ await outputAllOverviewCommand(profile, config, "grades", flags);
552
+ return;
553
+ }
554
+ const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
555
+ if (!studentInfo && !profile.studentNumber) {
556
+ await printStudentSelectionHelp(profile, config);
557
+ return;
558
+ }
559
+ const perStudentClient = await WilmaClient.login({
560
+ ...profile,
561
+ studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
562
+ });
563
+ await outputGrades(perStudentClient, {
564
+ limit: flags.limit ?? 20,
565
+ json: flags.json,
566
+ label: studentInfo?.name ?? undefined,
567
+ });
568
+ return;
569
+ }
570
+ if (command === "summary") {
571
+ if (flags.allStudents) {
572
+ await outputAllOverviewCommand(profile, config, "summary", flags);
573
+ return;
574
+ }
575
+ const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
576
+ if (!studentInfo && !profile.studentNumber) {
577
+ await printStudentSelectionHelp(profile, config);
578
+ return;
579
+ }
580
+ const perStudentClient = await WilmaClient.login({
581
+ ...profile,
582
+ studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
583
+ });
584
+ await outputSummary(perStudentClient, {
585
+ days: flags.days ?? 7,
586
+ json: flags.json,
587
+ label: studentInfo?.name ?? undefined,
588
+ });
589
+ return;
590
+ }
591
+ printUsage();
487
592
  }
488
593
  function printUsage() {
489
594
  console.log("Usage:");
595
+ console.log(" wilma summary [--days 7] [--student <id|name>] [--all-students] [--json]");
596
+ console.log(" wilma schedule list [--when today|tomorrow|week] [--date YYYY-MM-DD] [--weekday mon|tue|wed|thu|fri|sat|sun] [--student <id|name>] [--all-students] [--json]");
597
+ console.log(" wilma homework list [--limit 10] [--student <id|name>] [--all-students] [--json]");
598
+ console.log(" wilma exams list [--limit 20] [--student <id|name>] [--all-students] [--json]");
599
+ console.log(" wilma grades list [--limit 20] [--student <id|name>] [--all-students] [--json]");
490
600
  console.log(" wilma kids list [--json]");
491
601
  console.log(" wilma news list [--limit 20] [--student <id|name>] [--all-students] [--json]");
492
602
  console.log(" wilma news read <id> [--student <id|name>] [--json]");
493
603
  console.log(" wilma messages list [--folder inbox] [--limit 20] [--student <id|name>] [--all-students] [--json]");
494
604
  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
605
  console.log(" wilma update");
497
606
  console.log(" wilma config clear");
498
607
  console.log(" wilma --help | -h");
@@ -522,7 +631,10 @@ async function getProfileForCommandNonInteractive(config, flags) {
522
631
  };
523
632
  }
524
633
  function parseArgs(args) {
525
- const [command, subcommand, ...rest] = args;
634
+ const [command, rawSubcommand, ...rawRest] = args;
635
+ // If "subcommand" is actually a flag, push it back into rest
636
+ const subcommand = rawSubcommand?.startsWith("--") ? undefined : rawSubcommand;
637
+ const rest = rawSubcommand?.startsWith("--") ? [rawSubcommand, ...rawRest] : rawRest;
526
638
  const flags = {};
527
639
  let i = 0;
528
640
  while (i < rest.length) {
@@ -560,6 +672,29 @@ function parseArgs(args) {
560
672
  i += 2;
561
673
  continue;
562
674
  }
675
+ if (arg === "--when") {
676
+ flags.when = rest[i + 1];
677
+ i += 2;
678
+ continue;
679
+ }
680
+ if (arg === "--days") {
681
+ const value = Number(rest[i + 1]);
682
+ if (!Number.isNaN(value)) {
683
+ flags.days = value;
684
+ }
685
+ i += 2;
686
+ continue;
687
+ }
688
+ if (arg === "--date") {
689
+ flags.date = rest[i + 1];
690
+ i += 2;
691
+ continue;
692
+ }
693
+ if (arg === "--weekday") {
694
+ flags.weekday = rest[i + 1];
695
+ i += 2;
696
+ continue;
697
+ }
563
698
  if (!flags.id && !arg.startsWith("--")) {
564
699
  flags.id = arg;
565
700
  i += 1;
@@ -569,6 +704,83 @@ function parseArgs(args) {
569
704
  }
570
705
  return { command, subcommand, flags };
571
706
  }
707
+ function parseReadId(raw, entity) {
708
+ if (!raw) {
709
+ console.error(`Missing ${entity} id.`);
710
+ process.exit(1);
711
+ }
712
+ const id = Number(raw);
713
+ if (!Number.isInteger(id) || id <= 0) {
714
+ console.error(`Invalid ${entity} id "${raw}". Expected a positive integer.`);
715
+ process.exit(1);
716
+ }
717
+ return id;
718
+ }
719
+ function parseIsoDateOrExit(raw) {
720
+ const value = (raw ?? "").trim();
721
+ // Accept YYYY-MM-DD only.
722
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
723
+ console.error(`Invalid date "${raw}". Expected YYYY-MM-DD.`);
724
+ process.exit(1);
725
+ }
726
+ // Validate date is real.
727
+ const d = new Date(value + "T12:00:00Z");
728
+ if (Number.isNaN(d.getTime())) {
729
+ console.error(`Invalid date "${raw}". Expected a real calendar date.`);
730
+ process.exit(1);
731
+ }
732
+ const iso = d.toISOString().slice(0, 10);
733
+ if (iso !== value) {
734
+ console.error(`Invalid date "${raw}". Expected a real calendar date.`);
735
+ process.exit(1);
736
+ }
737
+ return value;
738
+ }
739
+ function normalizeWeekdayOrExit(raw) {
740
+ const v = (raw ?? "").trim().toLowerCase();
741
+ const map = {
742
+ sun: 0,
743
+ su: 0,
744
+ sunday: 0,
745
+ mon: 1,
746
+ ma: 1,
747
+ monday: 1,
748
+ tue: 2,
749
+ ti: 2,
750
+ tuesday: 2,
751
+ wed: 3,
752
+ ke: 3,
753
+ wednesday: 3,
754
+ thu: 4,
755
+ to: 4,
756
+ thursday: 4,
757
+ fri: 5,
758
+ pe: 5,
759
+ friday: 5,
760
+ sat: 6,
761
+ la: 6,
762
+ saturday: 6,
763
+ };
764
+ const day = map[v];
765
+ if (day === undefined) {
766
+ console.error(`Invalid weekday "${raw}". Use mon|tue|wed|thu|fri|sat|sun (also accepts fi: ma|ti|ke|to|pe|la|su).`);
767
+ process.exit(1);
768
+ }
769
+ return day;
770
+ }
771
+ function nextDateForWeekday(rawWeekday) {
772
+ const target = normalizeWeekdayOrExit(rawWeekday);
773
+ const now = new Date();
774
+ // Use local time.
775
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 12, 0, 0);
776
+ const current = today.getDay();
777
+ let delta = (target - current + 7) % 7;
778
+ // If you ask for e.g. "thu" on a Saturday, you'll get next Thursday.
779
+ // If you ask for today's weekday, you'll get today.
780
+ const d = new Date(today);
781
+ d.setDate(d.getDate() + delta);
782
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
783
+ }
572
784
  async function readPackageVersion() {
573
785
  const pkgPath = resolve(dirname(new URL(import.meta.url).pathname), "..", "package.json");
574
786
  const raw = await readFile(pkgPath, "utf-8");
@@ -726,6 +938,243 @@ async function outputExams(client, opts) {
726
938
  console.log(`- ${prefix}${date} ${compactText(exam.subject)}`);
727
939
  });
728
940
  }
941
+ /* ------------------------------------------------------------------ */
942
+ /* Overview-powered output functions */
943
+ /* ------------------------------------------------------------------ */
944
+ function todayString() {
945
+ const d = new Date();
946
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
947
+ }
948
+ function nextSchoolDay(from) {
949
+ const d = from ? new Date(from + "T12:00:00") : new Date();
950
+ d.setDate(d.getDate() + 1);
951
+ // Skip Saturday (6) and Sunday (0)
952
+ while (d.getDay() === 0 || d.getDay() === 6) {
953
+ d.setDate(d.getDate() + 1);
954
+ }
955
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
956
+ }
957
+ function currentWeekBounds() {
958
+ const now = new Date();
959
+ const dayOfWeek = now.getDay(); // 0=Sun, 1=Mon, ...
960
+ const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
961
+ const monday = new Date(now);
962
+ monday.setDate(now.getDate() + diffToMonday);
963
+ const friday = new Date(monday);
964
+ friday.setDate(monday.getDate() + 4);
965
+ const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
966
+ return [fmt(monday), fmt(friday)];
967
+ }
968
+ const DAY_NAMES = ["Su", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
969
+ async function outputSchedule(client, opts) {
970
+ const overview = await client.overview.get();
971
+ const when = opts.when || "week";
972
+ let startDate;
973
+ let endDate;
974
+ if (opts.date && opts.weekday) {
975
+ console.error("Use either --date or --weekday, not both.");
976
+ process.exit(1);
977
+ }
978
+ if (opts.date) {
979
+ const parsed = parseIsoDateOrExit(opts.date);
980
+ startDate = endDate = parsed;
981
+ }
982
+ else if (opts.weekday) {
983
+ const parsed = nextDateForWeekday(opts.weekday);
984
+ startDate = endDate = parsed;
985
+ }
986
+ else if (when === "today") {
987
+ startDate = endDate = todayString();
988
+ }
989
+ else if (when === "tomorrow") {
990
+ startDate = endDate = nextSchoolDay();
991
+ }
992
+ else {
993
+ [startDate, endDate] = currentWeekBounds();
994
+ }
995
+ const lessons = overview.schedule.filter((l) => l.date >= startDate && l.date <= endDate);
996
+ if (opts.json) {
997
+ const result = !opts.date && !opts.weekday && when === "week"
998
+ ? { when, weekStart: startDate, weekEnd: endDate, lessons }
999
+ : { when: opts.date ? "date" : (opts.weekday ? "weekday" : when), date: startDate, lessons };
1000
+ console.log(JSON.stringify(result, null, 2));
1001
+ return;
1002
+ }
1003
+ const prefix = opts.label ? `[${opts.label}] ` : "";
1004
+ if (opts.date || opts.weekday) {
1005
+ console.log(`\n${prefix}Schedule for ${startDate}`);
1006
+ }
1007
+ else if (when === "today") {
1008
+ console.log(`\n${prefix}Schedule for today (${startDate})`);
1009
+ }
1010
+ else if (when === "tomorrow") {
1011
+ console.log(`\n${prefix}Schedule for tomorrow (${startDate})`);
1012
+ }
1013
+ else {
1014
+ console.log(`\n${prefix}Schedule for ${startDate} – ${endDate}`);
1015
+ }
1016
+ if (!lessons.length) {
1017
+ console.log(" No lessons found.");
1018
+ return;
1019
+ }
1020
+ let currentDate = "";
1021
+ for (const l of lessons) {
1022
+ if (l.date !== currentDate) {
1023
+ currentDate = l.date;
1024
+ const d = new Date(l.date + "T12:00:00");
1025
+ console.log(` ${DAY_NAMES[d.getDay()]} ${l.date}`);
1026
+ }
1027
+ const teacher = l.teacherCode ? ` - ${l.teacher}` : "";
1028
+ console.log(` ${l.start}-${l.end} ${l.subject}${teacher}`);
1029
+ }
1030
+ }
1031
+ async function outputHomework(client, opts) {
1032
+ const overview = await client.overview.get();
1033
+ const slice = overview.homework.slice(0, opts.limit);
1034
+ if (opts.json) {
1035
+ console.log(JSON.stringify(slice, null, 2));
1036
+ return;
1037
+ }
1038
+ const prefix = opts.label ? `[${opts.label}] ` : "";
1039
+ console.log(`\n${prefix}Homework (${overview.homework.length})`);
1040
+ slice.forEach((hw) => {
1041
+ const text = compactText(hw.homework);
1042
+ console.log(`- ${hw.date} ${hw.subject}: ${text}`);
1043
+ });
1044
+ }
1045
+ async function outputUpcomingExams(client, opts) {
1046
+ const overview = await client.overview.get();
1047
+ const slice = overview.upcomingExams.slice(0, opts.limit);
1048
+ if (opts.json) {
1049
+ console.log(JSON.stringify(slice, null, 2));
1050
+ return;
1051
+ }
1052
+ const prefix = opts.label ? `[${opts.label}] ` : "";
1053
+ console.log(`\n${prefix}Upcoming exams (${overview.upcomingExams.length})`);
1054
+ slice.forEach((exam) => {
1055
+ const topic = exam.topic ? ` — ${compactText(exam.topic)}` : "";
1056
+ console.log(`- ${exam.date} ${exam.subject}: ${exam.name}${topic}`);
1057
+ });
1058
+ }
1059
+ async function outputGrades(client, opts) {
1060
+ const overview = await client.overview.get();
1061
+ const slice = overview.grades.slice(0, opts.limit);
1062
+ if (opts.json) {
1063
+ console.log(JSON.stringify(slice, null, 2));
1064
+ return;
1065
+ }
1066
+ const prefix = opts.label ? `[${opts.label}] ` : "";
1067
+ console.log(`\n${prefix}Grades (${overview.grades.length})`);
1068
+ slice.forEach((g) => {
1069
+ console.log(`- ${g.date} ${g.subject}: ${g.name} — ${g.grade}`);
1070
+ });
1071
+ }
1072
+ async function outputSummary(client, opts) {
1073
+ const [overview, news, messages] = await Promise.all([
1074
+ client.overview.get(),
1075
+ client.news.list(),
1076
+ client.messages.list("inbox"),
1077
+ ]);
1078
+ const summary = buildSummaryData(overview, news, messages, opts.days, opts.label);
1079
+ if (opts.json) {
1080
+ console.log(JSON.stringify(summary, null, 2));
1081
+ return;
1082
+ }
1083
+ const label = summary.student ? ` for ${summary.student}` : "";
1084
+ console.log(`\nSummary${label} (${summary.today})`);
1085
+ console.log(`\nTODAY (${summary.today})`);
1086
+ if (summary.todaySchedule.length) {
1087
+ summary.todaySchedule.forEach((l) => {
1088
+ console.log(` ${l.start}-${l.end} ${l.subject} (${l.subjectCode})`);
1089
+ });
1090
+ }
1091
+ else {
1092
+ console.log(" No lessons today.");
1093
+ }
1094
+ console.log(`\nTOMORROW (${summary.tomorrow})`);
1095
+ if (summary.tomorrowSchedule.length) {
1096
+ summary.tomorrowSchedule.forEach((l) => {
1097
+ console.log(` ${l.start}-${l.end} ${l.subject} (${l.subjectCode})`);
1098
+ });
1099
+ }
1100
+ else {
1101
+ console.log(" No lessons tomorrow.");
1102
+ }
1103
+ if (summary.upcomingExams.length) {
1104
+ console.log("\nUPCOMING EXAMS");
1105
+ summary.upcomingExams.forEach((exam) => {
1106
+ const topic = exam.topic ? ` — ${compactText(exam.topic)}` : "";
1107
+ console.log(` ${exam.date} ${exam.subject}: ${exam.name}${topic}`);
1108
+ });
1109
+ }
1110
+ if (summary.recentHomework.length) {
1111
+ console.log("\nRECENT HOMEWORK");
1112
+ summary.recentHomework.forEach((hw) => {
1113
+ console.log(` ${hw.date} ${hw.subject}: ${compactText(hw.homework)}`);
1114
+ });
1115
+ }
1116
+ if (summary.recentNews.length) {
1117
+ console.log(`\nNEWS (last ${opts.days} days)`);
1118
+ summary.recentNews.forEach((n) => {
1119
+ const date = n.published ? n.published.slice(0, 10) : "";
1120
+ console.log(` ${date} ${compactText(n.title)} (id:${n.wilmaId})`);
1121
+ });
1122
+ }
1123
+ if (summary.recentMessages.length) {
1124
+ console.log(`\nMESSAGES (last ${opts.days} days)`);
1125
+ summary.recentMessages.forEach((m) => {
1126
+ const date = m.sentAt.slice(0, 10);
1127
+ console.log(` ${date} ${compactText(m.subject)} (id:${m.wilmaId})`);
1128
+ });
1129
+ }
1130
+ }
1131
+ function buildSummaryData(overview, news, messages, days, studentLabel) {
1132
+ const today = todayString();
1133
+ const tomorrow = nextSchoolDay();
1134
+ const cutoffDate = (() => {
1135
+ const d = new Date();
1136
+ d.setDate(d.getDate() - days);
1137
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1138
+ })();
1139
+ const homeworkCutoff = (() => {
1140
+ const d = new Date();
1141
+ d.setDate(d.getDate() - 3);
1142
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1143
+ })();
1144
+ const todaySchedule = overview.schedule.filter((l) => l.date === today);
1145
+ const tomorrowSchedule = overview.schedule.filter((l) => l.date === tomorrow);
1146
+ const upcomingExams = overview.upcomingExams;
1147
+ const recentHomework = overview.homework.filter((h) => h.date >= homeworkCutoff);
1148
+ const recentNews = news
1149
+ .filter((n) => n.published && n.published.toISOString().slice(0, 10) >= cutoffDate)
1150
+ .slice(0, 5)
1151
+ .map((n) => ({
1152
+ wilmaId: n.wilmaId,
1153
+ title: n.title,
1154
+ published: n.published?.toISOString() ?? null,
1155
+ }));
1156
+ const recentMessages = messages
1157
+ .filter((m) => m.sentAt.toISOString().slice(0, 10) >= cutoffDate)
1158
+ .slice(0, 5)
1159
+ .map((m) => ({
1160
+ wilmaId: m.wilmaId,
1161
+ subject: m.subject,
1162
+ sentAt: m.sentAt.toISOString(),
1163
+ senderName: m.senderName ?? null,
1164
+ }));
1165
+ return {
1166
+ generatedAt: new Date().toISOString(),
1167
+ student: studentLabel ?? null,
1168
+ today,
1169
+ tomorrow,
1170
+ todaySchedule,
1171
+ tomorrowSchedule,
1172
+ upcomingExams,
1173
+ recentHomework,
1174
+ recentNews,
1175
+ recentMessages,
1176
+ };
1177
+ }
729
1178
  async function outputMessages(client, opts) {
730
1179
  const messages = await client.messages.list(opts.folder);
731
1180
  const slice = messages.slice(0, opts.limit);
@@ -909,6 +1358,10 @@ async function resolveStudentForFlags(profile, config, student) {
909
1358
  const exact = students.find((s) => s.studentNumber === student);
910
1359
  if (exact)
911
1360
  return exact;
1361
+ const needle = student.toLowerCase();
1362
+ const substring = students.find((s) => s.name.toLowerCase().includes(needle));
1363
+ if (substring)
1364
+ return substring;
912
1365
  const match = students.find((s) => fuzzyIncludes(s.name, student));
913
1366
  if (match)
914
1367
  return match;
@@ -976,8 +1429,8 @@ async function outputAllExams(profile, config, limit, json) {
976
1429
  const results = [];
977
1430
  for (const student of students) {
978
1431
  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) });
1432
+ const overview = await client.overview.get();
1433
+ results.push({ student, items: overview.upcomingExams.slice(0, limit) });
981
1434
  }
982
1435
  if (json) {
983
1436
  console.log(JSON.stringify({ students: results }, null, 2));
@@ -986,11 +1439,101 @@ async function outputAllExams(profile, config, limit, json) {
986
1439
  results.forEach((entry) => {
987
1440
  console.log(`\n[${entry.student.name}]`);
988
1441
  entry.items.forEach((exam) => {
989
- const date = exam.examDate.toISOString().slice(0, 10);
990
- console.log(`- ${date} ${exam.subject}`);
1442
+ const topic = exam.topic ? ` — ${compactText(exam.topic)}` : "";
1443
+ console.log(`- ${exam.date} ${exam.subject}: ${exam.name}${topic}`);
991
1444
  });
992
1445
  });
993
1446
  }
1447
+ async function outputAllOverviewCommand(profile, config, command, flags) {
1448
+ const students = await getStudentsForCommand(profile, config);
1449
+ if (command === "summary") {
1450
+ if (flags.json) {
1451
+ const summaries = [];
1452
+ for (const student of students) {
1453
+ const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
1454
+ const [overview, news, messages] = await Promise.all([
1455
+ client.overview.get(),
1456
+ client.news.list(),
1457
+ client.messages.list("inbox"),
1458
+ ]);
1459
+ summaries.push({
1460
+ student,
1461
+ summary: buildSummaryData(overview, news, messages, flags.days ?? 7, student.name),
1462
+ });
1463
+ }
1464
+ console.log(JSON.stringify({
1465
+ generatedAt: new Date().toISOString(),
1466
+ students: summaries,
1467
+ }, null, 2));
1468
+ return;
1469
+ }
1470
+ // Human-readable summary output per student
1471
+ for (const student of students) {
1472
+ const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
1473
+ await outputSummary(client, {
1474
+ days: flags.days ?? 7,
1475
+ json: false,
1476
+ label: student.name,
1477
+ });
1478
+ }
1479
+ return;
1480
+ }
1481
+ const results = [];
1482
+ for (const student of students) {
1483
+ const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
1484
+ const overview = await client.overview.get();
1485
+ if (command === "schedule") {
1486
+ const when = flags.when || "week";
1487
+ let startDate, endDate;
1488
+ if (when === "today") {
1489
+ startDate = endDate = todayString();
1490
+ }
1491
+ else if (when === "tomorrow") {
1492
+ startDate = endDate = nextSchoolDay();
1493
+ }
1494
+ else {
1495
+ [startDate, endDate] = currentWeekBounds();
1496
+ }
1497
+ results.push({
1498
+ student,
1499
+ data: overview.schedule.filter((l) => l.date >= startDate && l.date <= endDate),
1500
+ });
1501
+ }
1502
+ else if (command === "homework") {
1503
+ results.push({ student, data: overview.homework.slice(0, flags.limit ?? 10) });
1504
+ }
1505
+ else if (command === "grades") {
1506
+ results.push({ student, data: overview.grades.slice(0, flags.limit ?? 20) });
1507
+ }
1508
+ }
1509
+ if (flags.json) {
1510
+ console.log(JSON.stringify({ students: results.map((r) => ({ student: r.student, items: r.data })) }, null, 2));
1511
+ return;
1512
+ }
1513
+ for (const r of results) {
1514
+ console.log(`\n[${r.student.name}]`);
1515
+ const items = r.data;
1516
+ if (!items.length) {
1517
+ console.log(" (none)");
1518
+ continue;
1519
+ }
1520
+ if (command === "schedule") {
1521
+ for (const l of items) {
1522
+ console.log(` ${l.date} ${l.start}-${l.end} ${l.subject} - ${l.teacher}`);
1523
+ }
1524
+ }
1525
+ else if (command === "homework") {
1526
+ for (const hw of items) {
1527
+ console.log(` ${hw.date} ${hw.subject}: ${compactText(hw.homework)}`);
1528
+ }
1529
+ }
1530
+ else if (command === "grades") {
1531
+ for (const g of items) {
1532
+ console.log(` ${g.date} ${g.subject}: ${g.name} — ${g.grade}`);
1533
+ }
1534
+ }
1535
+ }
1536
+ }
994
1537
  async function printStudentSelectionHelp(profile, config) {
995
1538
  const students = await getStudentsForCommand(profile, config);
996
1539
  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.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,9 +8,17 @@
8
8
  "wilmai": "dist/index.js",
9
9
  "wilma": "dist/index.js"
10
10
  },
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "lint": "echo 'add lint'",
14
+ "test": "echo 'add tests'",
15
+ "start": "node dist/index.js",
16
+ "prepack": "pnpm --filter @wilm-ai/wilma-client build && pnpm --filter @wilm-ai/wilma-cli build",
17
+ "test:live": "tsc -p tsconfig.json && node test/live.spec.mjs"
18
+ },
11
19
  "dependencies": {
12
20
  "@inquirer/prompts": "^5.3.8",
13
- "@wilm-ai/wilma-client": "^0.0.4"
21
+ "@wilm-ai/wilma-client": "workspace:^"
14
22
  },
15
23
  "devDependencies": {
16
24
  "typescript": "^5.6.3"
@@ -21,12 +29,5 @@
21
29
  ],
22
30
  "publishConfig": {
23
31
  "access": "public"
24
- },
25
- "scripts": {
26
- "build": "tsc -p tsconfig.json",
27
- "lint": "echo 'add lint'",
28
- "test": "echo 'add tests'",
29
- "start": "node dist/index.js",
30
- "test:live": "tsc -p tsconfig.json && node test/live.spec.mjs"
31
32
  }
32
- }
33
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Wilm.ai
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.