@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.
- package/README.md +42 -5
- package/dist/index.js +599 -26
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,11 +16,47 @@ wilma
|
|
|
16
16
|
wilmai
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
##
|
|
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
|
|
23
|
-
wilma
|
|
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 === "
|
|
144
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
|
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,
|
|
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
|
|
858
|
-
results.push({ student, items:
|
|
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
|
|
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": "
|
|
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": "^
|
|
13
|
+
"@wilm-ai/wilma-client": "^1.1.0"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"typescript": "^5.6.3"
|