@wilm-ai/wilma-cli 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/index.js +207 -34
- package/package.json +11 -10
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -26,7 +26,19 @@ Combines today's and tomorrow's schedule, upcoming exams, recent homework, news,
|
|
|
26
26
|
|
|
27
27
|
### Schedule
|
|
28
28
|
```bash
|
|
29
|
-
wilma schedule list [--when today|tomorrow|week] [--student <id|name>] [--all-students] [--json]
|
|
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
|
|
30
42
|
```
|
|
31
43
|
|
|
32
44
|
### Homework
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { emitKeypressEvents } from "node:readline";
|
|
3
3
|
import { select, input, password } from "@inquirer/prompts";
|
|
4
|
+
import { createHmac } from "node:crypto";
|
|
4
5
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
5
6
|
import { spawn } from "node:child_process";
|
|
6
7
|
import { dirname, resolve } from "node:path";
|
|
7
|
-
import { WilmaClient, listTenants, } from "@wilm-ai/wilma-client";
|
|
8
|
+
import { WilmaClient, MfaRequiredError, listTenants, } from "@wilm-ai/wilma-client";
|
|
8
9
|
import { clearConfig, getConfigPath, loadConfig, obfuscateSecret, revealSecret, saveConfig, } from "./config.js";
|
|
9
10
|
// Enable keypress events for escape key detection
|
|
10
11
|
if (process.stdin.isTTY) {
|
|
@@ -142,7 +143,8 @@ async function runInteractive(config) {
|
|
|
142
143
|
if (!profile) {
|
|
143
144
|
return;
|
|
144
145
|
}
|
|
145
|
-
const
|
|
146
|
+
const interactiveMfa = createInteractiveMfaCallback();
|
|
147
|
+
const client = await WilmaClient.login(profile, interactiveMfa);
|
|
146
148
|
let nextAction = await selectOrCancel({
|
|
147
149
|
message: "What do you want to view?",
|
|
148
150
|
pageSize: 15,
|
|
@@ -377,7 +379,8 @@ async function handleCommand(args, config) {
|
|
|
377
379
|
const profile = await getProfileForCommandNonInteractive(config, flags);
|
|
378
380
|
if (!profile)
|
|
379
381
|
return;
|
|
380
|
-
const
|
|
382
|
+
const mfaCallback = createNonInteractiveMfaCallback(flags.totpSecret);
|
|
383
|
+
const client = await WilmaClient.login(profile, mfaCallback);
|
|
381
384
|
if (command === "kids") {
|
|
382
385
|
const students = await getStudentsForCommand(profile, config);
|
|
383
386
|
if (flags.json) {
|
|
@@ -409,12 +412,12 @@ async function handleCommand(args, config) {
|
|
|
409
412
|
const perStudentClient = await WilmaClient.login({
|
|
410
413
|
...profile,
|
|
411
414
|
studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
|
|
412
|
-
});
|
|
415
|
+
}, mfaCallback);
|
|
413
416
|
await outputNewsItem(perStudentClient, newsId, flags.json);
|
|
414
417
|
return;
|
|
415
418
|
}
|
|
416
419
|
if (flags.allStudents) {
|
|
417
|
-
await outputAllNews(profile, config, flags.limit ?? 20, flags.json);
|
|
420
|
+
await outputAllNews(profile, config, flags.limit ?? 20, flags.json, mfaCallback);
|
|
418
421
|
return;
|
|
419
422
|
}
|
|
420
423
|
const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
|
|
@@ -425,7 +428,7 @@ async function handleCommand(args, config) {
|
|
|
425
428
|
const perStudentClient = await WilmaClient.login({
|
|
426
429
|
...profile,
|
|
427
430
|
studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
|
|
428
|
-
});
|
|
431
|
+
}, mfaCallback);
|
|
429
432
|
await outputNews(perStudentClient, {
|
|
430
433
|
limit: flags.limit ?? 20,
|
|
431
434
|
json: flags.json,
|
|
@@ -452,7 +455,7 @@ async function handleCommand(args, config) {
|
|
|
452
455
|
const perStudentClient = await WilmaClient.login({
|
|
453
456
|
...profile,
|
|
454
457
|
studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
|
|
455
|
-
});
|
|
458
|
+
}, mfaCallback);
|
|
456
459
|
await outputMessageItem(perStudentClient, messageId, flags.json);
|
|
457
460
|
return;
|
|
458
461
|
}
|
|
@@ -461,7 +464,7 @@ async function handleCommand(args, config) {
|
|
|
461
464
|
folder: flags.folder ?? "inbox",
|
|
462
465
|
limit: flags.limit ?? 20,
|
|
463
466
|
json: flags.json,
|
|
464
|
-
});
|
|
467
|
+
}, mfaCallback);
|
|
465
468
|
return;
|
|
466
469
|
}
|
|
467
470
|
const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
|
|
@@ -472,7 +475,7 @@ async function handleCommand(args, config) {
|
|
|
472
475
|
const perStudentClient = await WilmaClient.login({
|
|
473
476
|
...profile,
|
|
474
477
|
studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
|
|
475
|
-
});
|
|
478
|
+
}, mfaCallback);
|
|
476
479
|
await outputMessages(perStudentClient, {
|
|
477
480
|
folder: flags.folder ?? "inbox",
|
|
478
481
|
limit: flags.limit ?? 20,
|
|
@@ -483,7 +486,7 @@ async function handleCommand(args, config) {
|
|
|
483
486
|
}
|
|
484
487
|
if (command === "exams") {
|
|
485
488
|
if (flags.allStudents) {
|
|
486
|
-
await outputAllExams(profile, config, flags.limit ?? 20, flags.json);
|
|
489
|
+
await outputAllExams(profile, config, flags.limit ?? 20, flags.json, mfaCallback);
|
|
487
490
|
return;
|
|
488
491
|
}
|
|
489
492
|
const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
|
|
@@ -494,7 +497,7 @@ async function handleCommand(args, config) {
|
|
|
494
497
|
const perStudentClient = await WilmaClient.login({
|
|
495
498
|
...profile,
|
|
496
499
|
studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
|
|
497
|
-
});
|
|
500
|
+
}, mfaCallback);
|
|
498
501
|
await outputUpcomingExams(perStudentClient, {
|
|
499
502
|
limit: flags.limit ?? 20,
|
|
500
503
|
json: flags.json,
|
|
@@ -504,7 +507,7 @@ async function handleCommand(args, config) {
|
|
|
504
507
|
}
|
|
505
508
|
if (command === "schedule") {
|
|
506
509
|
if (flags.allStudents) {
|
|
507
|
-
await outputAllOverviewCommand(profile, config, "schedule", flags);
|
|
510
|
+
await outputAllOverviewCommand(profile, config, "schedule", flags, mfaCallback);
|
|
508
511
|
return;
|
|
509
512
|
}
|
|
510
513
|
const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
|
|
@@ -515,9 +518,11 @@ async function handleCommand(args, config) {
|
|
|
515
518
|
const perStudentClient = await WilmaClient.login({
|
|
516
519
|
...profile,
|
|
517
520
|
studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
|
|
518
|
-
});
|
|
521
|
+
}, mfaCallback);
|
|
519
522
|
await outputSchedule(perStudentClient, {
|
|
520
523
|
when: flags.when ?? "week",
|
|
524
|
+
date: flags.date,
|
|
525
|
+
weekday: flags.weekday,
|
|
521
526
|
json: flags.json,
|
|
522
527
|
label: studentInfo?.name ?? undefined,
|
|
523
528
|
});
|
|
@@ -525,7 +530,7 @@ async function handleCommand(args, config) {
|
|
|
525
530
|
}
|
|
526
531
|
if (command === "homework") {
|
|
527
532
|
if (flags.allStudents) {
|
|
528
|
-
await outputAllOverviewCommand(profile, config, "homework", flags);
|
|
533
|
+
await outputAllOverviewCommand(profile, config, "homework", flags, mfaCallback);
|
|
529
534
|
return;
|
|
530
535
|
}
|
|
531
536
|
const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
|
|
@@ -536,7 +541,7 @@ async function handleCommand(args, config) {
|
|
|
536
541
|
const perStudentClient = await WilmaClient.login({
|
|
537
542
|
...profile,
|
|
538
543
|
studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
|
|
539
|
-
});
|
|
544
|
+
}, mfaCallback);
|
|
540
545
|
await outputHomework(perStudentClient, {
|
|
541
546
|
limit: flags.limit ?? 10,
|
|
542
547
|
json: flags.json,
|
|
@@ -546,7 +551,7 @@ async function handleCommand(args, config) {
|
|
|
546
551
|
}
|
|
547
552
|
if (command === "grades") {
|
|
548
553
|
if (flags.allStudents) {
|
|
549
|
-
await outputAllOverviewCommand(profile, config, "grades", flags);
|
|
554
|
+
await outputAllOverviewCommand(profile, config, "grades", flags, mfaCallback);
|
|
550
555
|
return;
|
|
551
556
|
}
|
|
552
557
|
const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
|
|
@@ -557,7 +562,7 @@ async function handleCommand(args, config) {
|
|
|
557
562
|
const perStudentClient = await WilmaClient.login({
|
|
558
563
|
...profile,
|
|
559
564
|
studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
|
|
560
|
-
});
|
|
565
|
+
}, mfaCallback);
|
|
561
566
|
await outputGrades(perStudentClient, {
|
|
562
567
|
limit: flags.limit ?? 20,
|
|
563
568
|
json: flags.json,
|
|
@@ -567,7 +572,7 @@ async function handleCommand(args, config) {
|
|
|
567
572
|
}
|
|
568
573
|
if (command === "summary") {
|
|
569
574
|
if (flags.allStudents) {
|
|
570
|
-
await outputAllOverviewCommand(profile, config, "summary", flags);
|
|
575
|
+
await outputAllOverviewCommand(profile, config, "summary", flags, mfaCallback);
|
|
571
576
|
return;
|
|
572
577
|
}
|
|
573
578
|
const studentInfo = await resolveStudentForFlags(profile, config, flags.student);
|
|
@@ -578,7 +583,7 @@ async function handleCommand(args, config) {
|
|
|
578
583
|
const perStudentClient = await WilmaClient.login({
|
|
579
584
|
...profile,
|
|
580
585
|
studentNumber: studentInfo?.studentNumber ?? profile.studentNumber,
|
|
581
|
-
});
|
|
586
|
+
}, mfaCallback);
|
|
582
587
|
await outputSummary(perStudentClient, {
|
|
583
588
|
days: flags.days ?? 7,
|
|
584
589
|
json: flags.json,
|
|
@@ -591,7 +596,7 @@ async function handleCommand(args, config) {
|
|
|
591
596
|
function printUsage() {
|
|
592
597
|
console.log("Usage:");
|
|
593
598
|
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]");
|
|
599
|
+
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]");
|
|
595
600
|
console.log(" wilma homework list [--limit 10] [--student <id|name>] [--all-students] [--json]");
|
|
596
601
|
console.log(" wilma exams list [--limit 20] [--student <id|name>] [--all-students] [--json]");
|
|
597
602
|
console.log(" wilma grades list [--limit 20] [--student <id|name>] [--all-students] [--json]");
|
|
@@ -604,6 +609,8 @@ function printUsage() {
|
|
|
604
609
|
console.log(" wilma config clear");
|
|
605
610
|
console.log(" wilma --help | -h");
|
|
606
611
|
console.log(" wilma --version | -v");
|
|
612
|
+
console.log("");
|
|
613
|
+
console.log("MFA: --totp-secret <base32-key|otpauth://...>");
|
|
607
614
|
}
|
|
608
615
|
async function getProfileForCommandNonInteractive(config, flags) {
|
|
609
616
|
if (!config.lastProfileId) {
|
|
@@ -683,6 +690,21 @@ function parseArgs(args) {
|
|
|
683
690
|
i += 2;
|
|
684
691
|
continue;
|
|
685
692
|
}
|
|
693
|
+
if (arg === "--date") {
|
|
694
|
+
flags.date = rest[i + 1];
|
|
695
|
+
i += 2;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (arg === "--weekday") {
|
|
699
|
+
flags.weekday = rest[i + 1];
|
|
700
|
+
i += 2;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (arg === "--totp-secret") {
|
|
704
|
+
flags.totpSecret = rest[i + 1];
|
|
705
|
+
i += 2;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
686
708
|
if (!flags.id && !arg.startsWith("--")) {
|
|
687
709
|
flags.id = arg;
|
|
688
710
|
i += 1;
|
|
@@ -704,6 +726,71 @@ function parseReadId(raw, entity) {
|
|
|
704
726
|
}
|
|
705
727
|
return id;
|
|
706
728
|
}
|
|
729
|
+
function parseIsoDateOrExit(raw) {
|
|
730
|
+
const value = (raw ?? "").trim();
|
|
731
|
+
// Accept YYYY-MM-DD only.
|
|
732
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
733
|
+
console.error(`Invalid date "${raw}". Expected YYYY-MM-DD.`);
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
// Validate date is real.
|
|
737
|
+
const d = new Date(value + "T12:00:00Z");
|
|
738
|
+
if (Number.isNaN(d.getTime())) {
|
|
739
|
+
console.error(`Invalid date "${raw}". Expected a real calendar date.`);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
const iso = d.toISOString().slice(0, 10);
|
|
743
|
+
if (iso !== value) {
|
|
744
|
+
console.error(`Invalid date "${raw}". Expected a real calendar date.`);
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
return value;
|
|
748
|
+
}
|
|
749
|
+
function normalizeWeekdayOrExit(raw) {
|
|
750
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
751
|
+
const map = {
|
|
752
|
+
sun: 0,
|
|
753
|
+
su: 0,
|
|
754
|
+
sunday: 0,
|
|
755
|
+
mon: 1,
|
|
756
|
+
ma: 1,
|
|
757
|
+
monday: 1,
|
|
758
|
+
tue: 2,
|
|
759
|
+
ti: 2,
|
|
760
|
+
tuesday: 2,
|
|
761
|
+
wed: 3,
|
|
762
|
+
ke: 3,
|
|
763
|
+
wednesday: 3,
|
|
764
|
+
thu: 4,
|
|
765
|
+
to: 4,
|
|
766
|
+
thursday: 4,
|
|
767
|
+
fri: 5,
|
|
768
|
+
pe: 5,
|
|
769
|
+
friday: 5,
|
|
770
|
+
sat: 6,
|
|
771
|
+
la: 6,
|
|
772
|
+
saturday: 6,
|
|
773
|
+
};
|
|
774
|
+
const day = map[v];
|
|
775
|
+
if (day === undefined) {
|
|
776
|
+
console.error(`Invalid weekday "${raw}". Use mon|tue|wed|thu|fri|sat|sun (also accepts fi: ma|ti|ke|to|pe|la|su).`);
|
|
777
|
+
process.exit(1);
|
|
778
|
+
}
|
|
779
|
+
return day;
|
|
780
|
+
}
|
|
781
|
+
function nextDateForWeekday(rawWeekday) {
|
|
782
|
+
const target = normalizeWeekdayOrExit(rawWeekday);
|
|
783
|
+
const now = new Date();
|
|
784
|
+
// Use local time.
|
|
785
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 12, 0, 0);
|
|
786
|
+
const current = today.getDay();
|
|
787
|
+
let delta = (target - current + 7) % 7;
|
|
788
|
+
// If you ask for e.g. "thu" on a Saturday, you'll get next Thursday.
|
|
789
|
+
// If you ask for today's weekday, you'll get today.
|
|
790
|
+
const d = new Date(today);
|
|
791
|
+
d.setDate(d.getDate() + delta);
|
|
792
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
793
|
+
}
|
|
707
794
|
async function readPackageVersion() {
|
|
708
795
|
const pkgPath = resolve(dirname(new URL(import.meta.url).pathname), "..", "package.json");
|
|
709
796
|
const raw = await readFile(pkgPath, "utf-8");
|
|
@@ -894,7 +981,19 @@ async function outputSchedule(client, opts) {
|
|
|
894
981
|
const when = opts.when || "week";
|
|
895
982
|
let startDate;
|
|
896
983
|
let endDate;
|
|
897
|
-
if (
|
|
984
|
+
if (opts.date && opts.weekday) {
|
|
985
|
+
console.error("Use either --date or --weekday, not both.");
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
if (opts.date) {
|
|
989
|
+
const parsed = parseIsoDateOrExit(opts.date);
|
|
990
|
+
startDate = endDate = parsed;
|
|
991
|
+
}
|
|
992
|
+
else if (opts.weekday) {
|
|
993
|
+
const parsed = nextDateForWeekday(opts.weekday);
|
|
994
|
+
startDate = endDate = parsed;
|
|
995
|
+
}
|
|
996
|
+
else if (when === "today") {
|
|
898
997
|
startDate = endDate = todayString();
|
|
899
998
|
}
|
|
900
999
|
else if (when === "tomorrow") {
|
|
@@ -905,14 +1004,17 @@ async function outputSchedule(client, opts) {
|
|
|
905
1004
|
}
|
|
906
1005
|
const lessons = overview.schedule.filter((l) => l.date >= startDate && l.date <= endDate);
|
|
907
1006
|
if (opts.json) {
|
|
908
|
-
const result = when === "week"
|
|
1007
|
+
const result = !opts.date && !opts.weekday && when === "week"
|
|
909
1008
|
? { when, weekStart: startDate, weekEnd: endDate, lessons }
|
|
910
|
-
: { when, date: startDate, lessons };
|
|
1009
|
+
: { when: opts.date ? "date" : (opts.weekday ? "weekday" : when), date: startDate, lessons };
|
|
911
1010
|
console.log(JSON.stringify(result, null, 2));
|
|
912
1011
|
return;
|
|
913
1012
|
}
|
|
914
1013
|
const prefix = opts.label ? `[${opts.label}] ` : "";
|
|
915
|
-
if (
|
|
1014
|
+
if (opts.date || opts.weekday) {
|
|
1015
|
+
console.log(`\n${prefix}Schedule for ${startDate}`);
|
|
1016
|
+
}
|
|
1017
|
+
else if (when === "today") {
|
|
916
1018
|
console.log(`\n${prefix}Schedule for today (${startDate})`);
|
|
917
1019
|
}
|
|
918
1020
|
else if (when === "tomorrow") {
|
|
@@ -1177,6 +1279,68 @@ async function selectOrCancel(opts, clearScreen = true) {
|
|
|
1177
1279
|
process.stdin.removeListener("keypress", onKeypress);
|
|
1178
1280
|
}
|
|
1179
1281
|
}
|
|
1282
|
+
function createInteractiveMfaCallback() {
|
|
1283
|
+
return async (_formkey) => {
|
|
1284
|
+
const code = await input({ message: "Enter MFA code from authenticator app" });
|
|
1285
|
+
if (!code) {
|
|
1286
|
+
throw new Error("MFA cancelled");
|
|
1287
|
+
}
|
|
1288
|
+
return code.trim();
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function createNonInteractiveMfaCallback(totpSecret) {
|
|
1292
|
+
if (!totpSecret)
|
|
1293
|
+
return undefined;
|
|
1294
|
+
const secret = parseTotpSecret(totpSecret);
|
|
1295
|
+
return async (_formkey) => {
|
|
1296
|
+
const code = generateTOTP(secret);
|
|
1297
|
+
return code;
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function parseTotpSecret(raw) {
|
|
1301
|
+
// Accept either a bare base32 key or an otpauth:// URI
|
|
1302
|
+
if (raw.startsWith("otpauth://")) {
|
|
1303
|
+
const url = new URL(raw);
|
|
1304
|
+
const secret = url.searchParams.get("secret");
|
|
1305
|
+
if (!secret) {
|
|
1306
|
+
console.error("No 'secret' parameter found in otpauth:// URI.");
|
|
1307
|
+
process.exit(1);
|
|
1308
|
+
}
|
|
1309
|
+
return secret;
|
|
1310
|
+
}
|
|
1311
|
+
return raw.replace(/[\s-]/g, "");
|
|
1312
|
+
}
|
|
1313
|
+
function generateTOTP(secret) {
|
|
1314
|
+
// TOTP: RFC 6238 — HMAC-SHA1 based, 6-digit, 30-second period
|
|
1315
|
+
const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
1316
|
+
// Decode base32 secret
|
|
1317
|
+
const cleanSecret = secret.replace(/[\s=-]+/g, "").toUpperCase();
|
|
1318
|
+
let bits = "";
|
|
1319
|
+
for (const c of cleanSecret) {
|
|
1320
|
+
const val = base32Chars.indexOf(c);
|
|
1321
|
+
if (val === -1)
|
|
1322
|
+
throw new Error(`Invalid base32 character: ${c}`);
|
|
1323
|
+
bits += val.toString(2).padStart(5, "0");
|
|
1324
|
+
}
|
|
1325
|
+
const bytes = new Uint8Array(Math.floor(bits.length / 8));
|
|
1326
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1327
|
+
bytes[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
|
|
1328
|
+
}
|
|
1329
|
+
const epoch = Math.floor(Date.now() / 1000);
|
|
1330
|
+
const counter = Math.floor(epoch / 30);
|
|
1331
|
+
const counterBuf = Buffer.alloc(8);
|
|
1332
|
+
counterBuf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);
|
|
1333
|
+
counterBuf.writeUInt32BE(counter >>> 0, 4);
|
|
1334
|
+
const hmac = createHmac("sha1", Buffer.from(bytes));
|
|
1335
|
+
hmac.update(counterBuf);
|
|
1336
|
+
const hash = hmac.digest();
|
|
1337
|
+
const offset = hash[hash.length - 1] & 0x0f;
|
|
1338
|
+
const code = ((hash[offset] & 0x7f) << 24) |
|
|
1339
|
+
((hash[offset + 1] & 0xff) << 16) |
|
|
1340
|
+
((hash[offset + 2] & 0xff) << 8) |
|
|
1341
|
+
(hash[offset + 3] & 0xff);
|
|
1342
|
+
return (code % 1000000).toString().padStart(6, "0");
|
|
1343
|
+
}
|
|
1180
1344
|
async function inputOrCancel(opts) {
|
|
1181
1345
|
console.clear();
|
|
1182
1346
|
const prompt = input(opts, { clearPromptOnDone: true });
|
|
@@ -1292,11 +1456,11 @@ async function resolveStudentForFlags(profile, config, student) {
|
|
|
1292
1456
|
const students = await getStudentsForCommand(profile, config);
|
|
1293
1457
|
return students[0] ?? null;
|
|
1294
1458
|
}
|
|
1295
|
-
async function outputAllNews(profile, config, limit, json) {
|
|
1459
|
+
async function outputAllNews(profile, config, limit, json, onMfa) {
|
|
1296
1460
|
const students = await getStudentsForCommand(profile, config);
|
|
1297
1461
|
const results = [];
|
|
1298
1462
|
for (const student of students) {
|
|
1299
|
-
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
|
|
1463
|
+
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber }, onMfa);
|
|
1300
1464
|
const news = await client.news.list();
|
|
1301
1465
|
results.push({ student, items: news.slice(0, limit) });
|
|
1302
1466
|
}
|
|
@@ -1312,11 +1476,11 @@ async function outputAllNews(profile, config, limit, json) {
|
|
|
1312
1476
|
});
|
|
1313
1477
|
});
|
|
1314
1478
|
}
|
|
1315
|
-
async function outputAllMessages(profile, config, opts) {
|
|
1479
|
+
async function outputAllMessages(profile, config, opts, onMfa) {
|
|
1316
1480
|
const students = await getStudentsForCommand(profile, config);
|
|
1317
1481
|
const results = [];
|
|
1318
1482
|
for (const student of students) {
|
|
1319
|
-
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
|
|
1483
|
+
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber }, onMfa);
|
|
1320
1484
|
const messages = await client.messages.list(opts.folder);
|
|
1321
1485
|
results.push({ student, items: messages.slice(0, opts.limit) });
|
|
1322
1486
|
}
|
|
@@ -1332,11 +1496,11 @@ async function outputAllMessages(profile, config, opts) {
|
|
|
1332
1496
|
});
|
|
1333
1497
|
});
|
|
1334
1498
|
}
|
|
1335
|
-
async function outputAllExams(profile, config, limit, json) {
|
|
1499
|
+
async function outputAllExams(profile, config, limit, json, onMfa) {
|
|
1336
1500
|
const students = await getStudentsForCommand(profile, config);
|
|
1337
1501
|
const results = [];
|
|
1338
1502
|
for (const student of students) {
|
|
1339
|
-
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
|
|
1503
|
+
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber }, onMfa);
|
|
1340
1504
|
const overview = await client.overview.get();
|
|
1341
1505
|
results.push({ student, items: overview.upcomingExams.slice(0, limit) });
|
|
1342
1506
|
}
|
|
@@ -1352,13 +1516,13 @@ async function outputAllExams(profile, config, limit, json) {
|
|
|
1352
1516
|
});
|
|
1353
1517
|
});
|
|
1354
1518
|
}
|
|
1355
|
-
async function outputAllOverviewCommand(profile, config, command, flags) {
|
|
1519
|
+
async function outputAllOverviewCommand(profile, config, command, flags, onMfa) {
|
|
1356
1520
|
const students = await getStudentsForCommand(profile, config);
|
|
1357
1521
|
if (command === "summary") {
|
|
1358
1522
|
if (flags.json) {
|
|
1359
1523
|
const summaries = [];
|
|
1360
1524
|
for (const student of students) {
|
|
1361
|
-
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
|
|
1525
|
+
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber }, onMfa);
|
|
1362
1526
|
const [overview, news, messages] = await Promise.all([
|
|
1363
1527
|
client.overview.get(),
|
|
1364
1528
|
client.news.list(),
|
|
@@ -1377,7 +1541,7 @@ async function outputAllOverviewCommand(profile, config, command, flags) {
|
|
|
1377
1541
|
}
|
|
1378
1542
|
// Human-readable summary output per student
|
|
1379
1543
|
for (const student of students) {
|
|
1380
|
-
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
|
|
1544
|
+
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber }, onMfa);
|
|
1381
1545
|
await outputSummary(client, {
|
|
1382
1546
|
days: flags.days ?? 7,
|
|
1383
1547
|
json: false,
|
|
@@ -1388,7 +1552,7 @@ async function outputAllOverviewCommand(profile, config, command, flags) {
|
|
|
1388
1552
|
}
|
|
1389
1553
|
const results = [];
|
|
1390
1554
|
for (const student of students) {
|
|
1391
|
-
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber });
|
|
1555
|
+
const client = await WilmaClient.login({ ...profile, studentNumber: student.studentNumber }, onMfa);
|
|
1392
1556
|
const overview = await client.overview.get();
|
|
1393
1557
|
if (command === "schedule") {
|
|
1394
1558
|
const when = flags.when || "week";
|
|
@@ -1453,6 +1617,15 @@ main().catch((err) => {
|
|
|
1453
1617
|
if (isPromptCancel(err)) {
|
|
1454
1618
|
process.exit(0);
|
|
1455
1619
|
}
|
|
1620
|
+
if (err instanceof MfaRequiredError) {
|
|
1621
|
+
console.error("MFA is enabled on this Wilma account.");
|
|
1622
|
+
console.error("For non-interactive use, provide your TOTP secret:");
|
|
1623
|
+
console.error(" --totp-secret <base32-key>");
|
|
1624
|
+
console.error(" --totp-secret 'otpauth://totp/...'");
|
|
1625
|
+
console.error("Tip: export the key from your authenticator app (base32 string or otpauth:// URI).");
|
|
1626
|
+
console.error("For interactive use, run 'wilma' without arguments.");
|
|
1627
|
+
process.exit(1);
|
|
1628
|
+
}
|
|
1456
1629
|
console.error("CLI error:", err instanceof Error ? err.message : err);
|
|
1457
1630
|
process.exit(1);
|
|
1458
1631
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wilm-ai/wilma-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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": "
|
|
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.
|