@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.
Files changed (4) hide show
  1. package/README.md +13 -1
  2. package/dist/index.js +207 -34
  3. package/package.json +11 -10
  4. 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 client = await WilmaClient.login(profile);
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 client = await WilmaClient.login(profile);
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 (when === "today") {
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 (when === "today") {
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.1.0",
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": "^1.1.0"
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.