@wilm-ai/wilma-cli 0.0.10 → 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 +599 -26
  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
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { emitKeypressEvents } from "node:readline";
3
3
  import { select, input, password } from "@inquirer/prompts";
4
- import { readFile } from "node:fs/promises";
4
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
5
+ import { spawn } from "node:child_process";
5
6
  import { dirname, resolve } from "node:path";
6
7
  import { WilmaClient, listTenants, } from "@wilm-ai/wilma-client";
7
8
  import { clearConfig, getConfigPath, loadConfig, obfuscateSecret, revealSecret, saveConfig, } from "./config.js";
@@ -10,20 +11,33 @@ if (process.stdin.isTTY) {
10
11
  emitKeypressEvents(process.stdin);
11
12
  }
12
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" },
13
20
  { value: "news", name: "List news" },
14
- { value: "exams", name: "List exams" },
15
21
  { value: "messages", name: "List messages" },
16
22
  { value: "exit", name: "Exit" },
17
23
  ];
18
24
  async function main() {
19
25
  const args = process.argv.slice(2);
26
+ // Fire version check early (non-blocking)
27
+ const updateCheck = checkForUpdate();
20
28
  if (args.includes("--help") || args.includes("-h")) {
21
29
  printUsage();
30
+ await showUpdateNotice(updateCheck);
22
31
  return;
23
32
  }
24
33
  if (args.includes("--version") || args.includes("-v")) {
25
34
  const version = await readPackageVersion();
26
35
  console.log(version);
36
+ await showUpdateNotice(updateCheck);
37
+ return;
38
+ }
39
+ if (args[0] === "update") {
40
+ await handleUpdate();
27
41
  return;
28
42
  }
29
43
  if (args[0] === "config" && args[1] === "clear") {
@@ -34,9 +48,11 @@ async function main() {
34
48
  const config = await loadConfig();
35
49
  if (args.length) {
36
50
  await handleCommand(args, config);
51
+ await showUpdateNotice(updateCheck);
37
52
  return;
38
53
  }
39
54
  await runInteractive(config);
55
+ await showUpdateNotice(updateCheck);
40
56
  }
41
57
  async function chooseProfile(config) {
42
58
  if (config.profiles.length) {
@@ -129,6 +145,7 @@ async function runInteractive(config) {
129
145
  const client = await WilmaClient.login(profile);
130
146
  let nextAction = await selectOrCancel({
131
147
  message: "What do you want to view?",
148
+ pageSize: 15,
132
149
  choices: [
133
150
  ...ACTIONS.filter((a) => a.value !== "exit"),
134
151
  { value: "back", name: "Back to students" },
@@ -140,12 +157,32 @@ async function runInteractive(config) {
140
157
  continue;
141
158
  }
142
159
  while (nextAction !== "exit" && nextAction !== "back") {
143
- if (nextAction === "news") {
144
- 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 });
145
175
  }
146
176
  if (nextAction === "exams") {
147
177
  console.clear();
148
- 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);
149
186
  }
150
187
  if (nextAction === "messages") {
151
188
  const folder = await selectOrCancel({
@@ -164,6 +201,7 @@ async function runInteractive(config) {
164
201
  }
165
202
  nextAction = await selectOrCancel({
166
203
  message: "What next?",
204
+ pageSize: 15,
167
205
  choices: [
168
206
  ...ACTIONS.filter((a) => a.value !== "exit"),
169
207
  { value: "back", name: "Back to students" },
@@ -354,6 +392,7 @@ async function handleCommand(args, config) {
354
392
  }
355
393
  if (command === "news") {
356
394
  if (subcommand === "read" && flags.id) {
395
+ const newsId = parseReadId(flags.id, "news");
357
396
  if (!flags.student) {
358
397
  const students = await getStudentsForCommand(profile, config);
359
398
  if (students.length > 1) {
@@ -371,7 +410,7 @@ async function handleCommand(args, config) {
371
410
  ...profile,
372
411
  studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
373
412
  });
374
- await outputNewsItem(perStudentClient, Number(flags.id), flags.json);
413
+ await outputNewsItem(perStudentClient, newsId, flags.json);
375
414
  return;
376
415
  }
377
416
  if (flags.allStudents) {
@@ -396,6 +435,7 @@ async function handleCommand(args, config) {
396
435
  }
397
436
  if (command === "messages") {
398
437
  if (subcommand === "read" && flags.id) {
438
+ const messageId = parseReadId(flags.id, "message");
399
439
  if (!flags.student) {
400
440
  const students = await getStudentsForCommand(profile, config);
401
441
  if (students.length > 1) {
@@ -413,7 +453,7 @@ async function handleCommand(args, config) {
413
453
  ...profile,
414
454
  studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
415
455
  });
416
- await outputMessageItem(perStudentClient, Number(flags.id), flags.json);
456
+ await outputMessageItem(perStudentClient, messageId, flags.json);
417
457
  return;
418
458
  }
419
459
  if (flags.allStudents) {
@@ -455,32 +495,112 @@ async function handleCommand(args, config) {
455
495
  ...profile,
456
496
  studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
457
497
  });
458
- await outputExams(perStudentClient, {
498
+ await outputUpcomingExams(perStudentClient, {
459
499
  limit: flags.limit ?? 20,
460
500
  json: flags.json,
461
501
  label: studentInfo?.name ?? undefined,
462
502
  });
463
503
  return;
464
504
  }
465
- console.log("Usage:");
466
- console.log(" wilma kids list [--json]");
467
- console.log(" wilma news list [--limit 20] [--student <id|name>] [--all-students] [--json]");
468
- console.log(" wilma news read <id> [--json]");
469
- console.log(" wilma messages list [--folder inbox] [--limit 20] [--student <id|name>] [--all-students] [--json]");
470
- console.log(" wilma messages read <id> [--json]");
471
- console.log(" wilma exams list [--limit 20] [--student <id|name>] [--all-students] [--json]");
472
- console.log(" wilma config clear");
473
- console.log(" wilma --help | -h");
474
- 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();
475
590
  }
476
591
  function printUsage() {
477
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]");
478
598
  console.log(" wilma kids list [--json]");
479
599
  console.log(" wilma news list [--limit 20] [--student <id|name>] [--all-students] [--json]");
480
- console.log(" wilma news read <id> [--json]");
600
+ console.log(" wilma news read <id> [--student <id|name>] [--json]");
481
601
  console.log(" wilma messages list [--folder inbox] [--limit 20] [--student <id|name>] [--all-students] [--json]");
482
- console.log(" wilma messages read <id> [--json]");
483
- console.log(" wilma exams list [--limit 20] [--student <id|name>] [--all-students] [--json]");
602
+ console.log(" wilma messages read <id> [--student <id|name>] [--json]");
603
+ console.log(" wilma update");
484
604
  console.log(" wilma config clear");
485
605
  console.log(" wilma --help | -h");
486
606
  console.log(" wilma --version | -v");
@@ -509,7 +629,10 @@ async function getProfileForCommandNonInteractive(config, flags) {
509
629
  };
510
630
  }
511
631
  function parseArgs(args) {
512
- 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;
513
636
  const flags = {};
514
637
  let i = 0;
515
638
  while (i < rest.length) {
@@ -547,6 +670,19 @@ function parseArgs(args) {
547
670
  i += 2;
548
671
  continue;
549
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
+ }
550
686
  if (!flags.id && !arg.startsWith("--")) {
551
687
  flags.id = arg;
552
688
  i += 1;
@@ -556,12 +692,133 @@ function parseArgs(args) {
556
692
  }
557
693
  return { command, subcommand, flags };
558
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
+ }
559
707
  async function readPackageVersion() {
560
708
  const pkgPath = resolve(dirname(new URL(import.meta.url).pathname), "..", "package.json");
561
709
  const raw = await readFile(pkgPath, "utf-8");
562
710
  const data = JSON.parse(raw);
563
711
  return data.version ?? "unknown";
564
712
  }
713
+ async function handleUpdate() {
714
+ const currentVersion = await readPackageVersion();
715
+ console.log(`Current version: ${currentVersion}`);
716
+ console.log("Updating @wilm-ai/wilma-cli...\n");
717
+ return new Promise((resolve, reject) => {
718
+ const child = spawn("npm", ["install", "-g", "@wilm-ai/wilma-cli@latest"], {
719
+ stdio: "inherit",
720
+ shell: true,
721
+ });
722
+ child.on("error", (err) => {
723
+ if (err.code === "ENOENT") {
724
+ console.error("Error: npm not found. Please install npm and try again.");
725
+ }
726
+ else {
727
+ console.error("Update failed:", err.message);
728
+ }
729
+ reject(err);
730
+ });
731
+ child.on("close", (code) => {
732
+ if (code === 0) {
733
+ console.log("\nUpdate complete.");
734
+ resolve();
735
+ }
736
+ else {
737
+ console.error(`\nnpm exited with code ${code}`);
738
+ reject(new Error(`npm exited with code ${code}`));
739
+ }
740
+ });
741
+ });
742
+ }
743
+ // --- Version check / update notification ---
744
+ const VERSION_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
745
+ function getVersionCachePath() {
746
+ return resolve(dirname(getConfigPath()), "version-check.json");
747
+ }
748
+ async function readVersionCache() {
749
+ try {
750
+ const raw = await readFile(getVersionCachePath(), "utf-8");
751
+ return JSON.parse(raw);
752
+ }
753
+ catch {
754
+ return null;
755
+ }
756
+ }
757
+ async function writeVersionCache(cache) {
758
+ const cachePath = getVersionCachePath();
759
+ await mkdir(dirname(cachePath), { recursive: true });
760
+ await writeFile(cachePath, JSON.stringify(cache), "utf-8");
761
+ }
762
+ async function checkForUpdate() {
763
+ try {
764
+ const cache = await readVersionCache();
765
+ if (cache && Date.now() - cache.checkedAt < VERSION_CHECK_INTERVAL_MS) {
766
+ return cache.latestVersion;
767
+ }
768
+ const controller = new AbortController();
769
+ const timeout = setTimeout(() => controller.abort(), 3000);
770
+ try {
771
+ const response = await fetch("https://registry.npmjs.org/@wilm-ai/wilma-cli/latest", { signal: controller.signal });
772
+ clearTimeout(timeout);
773
+ if (!response.ok)
774
+ return cache?.latestVersion ?? null;
775
+ const data = (await response.json());
776
+ const latestVersion = data.version ?? null;
777
+ if (latestVersion) {
778
+ await writeVersionCache({ latestVersion, checkedAt: Date.now() });
779
+ }
780
+ return latestVersion;
781
+ }
782
+ catch {
783
+ clearTimeout(timeout);
784
+ return cache?.latestVersion ?? null;
785
+ }
786
+ }
787
+ catch {
788
+ return null;
789
+ }
790
+ }
791
+ function isNewerVersion(latest, current) {
792
+ const latestParts = latest.split(".").map(Number);
793
+ const currentParts = current.split(".").map(Number);
794
+ for (let i = 0; i < 3; i++) {
795
+ const l = latestParts[i] ?? 0;
796
+ const c = currentParts[i] ?? 0;
797
+ if (l > c)
798
+ return true;
799
+ if (l < c)
800
+ return false;
801
+ }
802
+ return false;
803
+ }
804
+ async function showUpdateNotice(versionCheckPromise) {
805
+ try {
806
+ const latestVersion = await Promise.race([
807
+ versionCheckPromise,
808
+ new Promise((resolve) => setTimeout(() => resolve(null), 1000)),
809
+ ]);
810
+ if (!latestVersion)
811
+ return;
812
+ const currentVersion = await readPackageVersion();
813
+ if (isNewerVersion(latestVersion, currentVersion)) {
814
+ process.stderr.write(`\nUpdate available: ${currentVersion} → ${latestVersion}\n` +
815
+ `Run "wilma update" to update.\n`);
816
+ }
817
+ }
818
+ catch {
819
+ // Silently ignore any errors
820
+ }
821
+ }
565
822
  async function outputNews(client, opts) {
566
823
  const news = await client.news.list();
567
824
  const slice = news.slice(0, opts.limit);
@@ -604,6 +861,228 @@ async function outputExams(client, opts) {
604
861
  console.log(`- ${prefix}${date} ${compactText(exam.subject)}`);
605
862
  });
606
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
+ }
607
1086
  async function outputMessages(client, opts) {
608
1087
  const messages = await client.messages.list(opts.folder);
609
1088
  const slice = messages.slice(0, opts.limit);
@@ -787,6 +1266,10 @@ async function resolveStudentForFlags(profile, config, student) {
787
1266
  const exact = students.find((s) => s.studentNumber === student);
788
1267
  if (exact)
789
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;
790
1273
  const match = students.find((s) => fuzzyIncludes(s.name, student));
791
1274
  if (match)
792
1275
  return match;
@@ -854,8 +1337,8 @@ async function outputAllExams(profile, config, limit, json) {
854
1337
  const results = [];
855
1338
  for (const student of students) {
856
1339
  const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
857
- const exams = await client.exams.list();
858
- 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) });
859
1342
  }
860
1343
  if (json) {
861
1344
  console.log(JSON.stringify({ students: results }, null, 2));
@@ -864,11 +1347,101 @@ async function outputAllExams(profile, config, limit, json) {
864
1347
  results.forEach((entry) => {
865
1348
  console.log(`\n[${entry.student.name}]`);
866
1349
  entry.items.forEach((exam) => {
867
- const date = exam.examDate.toISOString().slice(0, 10);
868
- console.log(`- ${date} ${exam.subject}`);
1350
+ const topic = exam.topic ? ` — ${compactText(exam.topic)}` : "";
1351
+ console.log(`- ${exam.date} ${exam.subject}: ${exam.name}${topic}`);
869
1352
  });
870
1353
  });
871
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
+ }
872
1445
  async function printStudentSelectionHelp(profile, config) {
873
1446
  const students = await getStudentsForCommand(profile, config);
874
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.10",
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"