@wilm-ai/wilma-cli 2.3.1 → 2.4.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 +2 -2
- package/dist/access.d.ts +18 -0
- package/dist/access.js +48 -3
- package/dist/agent-data.d.ts +184 -14
- package/dist/agent-data.js +195 -16
- package/dist/cli-args.js +1 -0
- package/dist/cli-help.js +23 -12
- package/dist/cli-output.d.ts +4 -1
- package/dist/cli-output.js +19 -5
- package/dist/config.d.ts +11 -4
- package/dist/config.js +4 -0
- package/dist/credentials.js +2 -2
- package/dist/index.js +37 -42
- package/dist/login-server.d.ts +26 -6
- package/dist/login-server.js +444 -227
- package/dist/mcp-tools.d.ts +2 -2
- package/dist/mcp-tools.js +48 -17
- package/dist/mcp.js +2 -14
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -7,9 +7,9 @@ import { homedir } from "node:os";
|
|
|
7
7
|
import { dirname, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { APIError, AuthenticationError, MfaRequiredError, NetworkError, WilmaClient, listTenants, } from "@wilm-ai/wilma-client";
|
|
10
|
-
import { clearConfig, getConfigPath, loadConfig, obfuscateSecret, revealSecret, saveConfig } from "./config.js";
|
|
11
|
-
import { WilmaAccess, WilmaAiError, adoptSession, flushSessions,
|
|
12
|
-
import { createAccess, logoutAccounts } from "./access.js";
|
|
10
|
+
import { clearConfig, getConfigPath, loadConfig, obfuscateSecret, revealSecret, saveConfig, savedStudent } from "./config.js";
|
|
11
|
+
import { WilmaAccess, WilmaAiError, adoptSession, flushSessions, roleOf, useSessionStore, } from "./agent-data.js";
|
|
12
|
+
import { createAccess, logoutAccounts, removeLogin } from "./access.js";
|
|
13
13
|
import { parseCliArgs } from "./cli-args.js";
|
|
14
14
|
import { commandHelp, generalHelp } from "./cli-help.js";
|
|
15
15
|
import { compactText, formatText, messageLine, newsLine } from "./cli-output.js";
|
|
@@ -52,6 +52,7 @@ const ACTIONS = [
|
|
|
52
52
|
{ value: "grades", name: "Exam grades" },
|
|
53
53
|
{ value: "notes", name: "Lesson notes (last 14 days)" },
|
|
54
54
|
{ value: "gradebook", name: "Gradebook" },
|
|
55
|
+
{ value: "contacts", name: "Class and teachers" },
|
|
55
56
|
{ value: "news", name: "List news" },
|
|
56
57
|
{ value: "messages", name: "List messages" },
|
|
57
58
|
{ value: "exit", name: "Exit" },
|
|
@@ -158,15 +159,17 @@ async function runDataCommand(parsed, out) {
|
|
|
158
159
|
case "summary":
|
|
159
160
|
return out.data(await access.summary({ student, days: flags.days, since: flags.since }));
|
|
160
161
|
case "schedule":
|
|
161
|
-
return out.data(await access.schedule({ student, when: flags.when, date: flags.date, weekday: flags.weekday }));
|
|
162
|
+
return out.data(await access.schedule({ student, when: flags.when, date: flags.date, weekday: flags.weekday, from: flags.from, to: flags.to }));
|
|
162
163
|
case "homework":
|
|
163
164
|
return out.data(await access.homework({ student, limit: flags.limit }));
|
|
164
165
|
case "exams":
|
|
165
166
|
return out.data(await access.upcomingExams({ student, limit: flags.limit }));
|
|
166
167
|
case "grades":
|
|
167
|
-
return out.data(await access.grades({ student, limit: flags.limit }));
|
|
168
|
+
return out.data(await access.grades({ student, limit: flags.limit, from: flags.from, to: flags.to }));
|
|
168
169
|
case "gradebook":
|
|
169
170
|
return out.data(await access.gradebook({ student }));
|
|
171
|
+
case "contacts":
|
|
172
|
+
return out.data(await access.contacts({ student }));
|
|
170
173
|
case "notes":
|
|
171
174
|
if (action === "summary")
|
|
172
175
|
return out.data(await access.lessonNotesSummary({ student, from: flags.from, to: flags.to }));
|
|
@@ -266,6 +269,8 @@ async function runInteractive(config) {
|
|
|
266
269
|
show("notes", await access.lessonNotes({ student, days: 14 }));
|
|
267
270
|
if (nextAction === "gradebook")
|
|
268
271
|
show("gradebook", await access.gradebook({ student }));
|
|
272
|
+
if (nextAction === "contacts")
|
|
273
|
+
show("contacts", await access.contacts({ student }));
|
|
269
274
|
if (nextAction === "news")
|
|
270
275
|
await selectNewsToRead(access, student);
|
|
271
276
|
if (nextAction === "messages") {
|
|
@@ -528,7 +533,7 @@ async function chooseStudentFromProfile(stored, baseProfile, onMfa) {
|
|
|
528
533
|
});
|
|
529
534
|
if (fresh.length) {
|
|
530
535
|
students = fresh;
|
|
531
|
-
stored.students = fresh.map(
|
|
536
|
+
stored.students = fresh.map(savedStudent);
|
|
532
537
|
}
|
|
533
538
|
else if (stored.students && stored.students.length) {
|
|
534
539
|
students = stored.students.map((s) => ({
|
|
@@ -632,7 +637,7 @@ async function handleLogin(args, out) {
|
|
|
632
637
|
console.log(`\nLogged in to ${stored.tenantName ?? stored.tenantUrl} as ${stored.username}.`);
|
|
633
638
|
if (students.length)
|
|
634
639
|
console.log(`Children: ${students.join(", ")}`);
|
|
635
|
-
console.log("Add another Wilma in the browser if a child is on a different one, or press Done there (or Enter here) to finish.");
|
|
640
|
+
console.log("Add another school's Wilma in the browser if a child is on a different one, or press Done there (or Enter here) to finish.");
|
|
636
641
|
},
|
|
637
642
|
});
|
|
638
643
|
const opened = flags.browser && openBrowser(server.url) === "opened";
|
|
@@ -655,22 +660,27 @@ async function handleLogin(args, out) {
|
|
|
655
660
|
process.stdin.off("data", finishOnEnter);
|
|
656
661
|
process.stdin.pause();
|
|
657
662
|
}
|
|
658
|
-
|
|
663
|
+
// Done without a new login is fine when logins are saved already: the page also logs in again, unticks and removes.
|
|
664
|
+
const saved = (await loadConfig()).profiles;
|
|
665
|
+
if (!saved.length) {
|
|
659
666
|
throw new WilmaAiError("login_cancelled", "Login page closed before logging in. Run `wilma login` again.");
|
|
660
667
|
}
|
|
661
668
|
await flushSessions();
|
|
662
669
|
if (out.json) {
|
|
670
|
+
// Every saved login, as the page left them.
|
|
663
671
|
out.data({
|
|
664
672
|
status: "ok",
|
|
665
|
-
logins:
|
|
673
|
+
logins: saved.map((stored) => ({
|
|
666
674
|
wilma: stored.tenantName ?? stored.tenantUrl,
|
|
667
675
|
username: stored.username,
|
|
668
|
-
students: (stored.students ?? []).map((s) => s.name),
|
|
676
|
+
students: (stored.students ?? []).filter((s) => !stored.hidden?.includes(s.studentNumber)).map((s) => s.name),
|
|
669
677
|
})),
|
|
670
678
|
configPath: getConfigPath(),
|
|
671
679
|
});
|
|
672
680
|
return;
|
|
673
681
|
}
|
|
682
|
+
if (!logins.length)
|
|
683
|
+
console.log(`\nNo new login. On this computer: ${saved.map((p) => `${p.tenantName ?? p.tenantUrl} — ${p.username}`).join(", ")}.`);
|
|
674
684
|
console.log(`\nSaved to ${getConfigPath()}`);
|
|
675
685
|
}
|
|
676
686
|
const HEADLESS_LOGIN_HELP = [
|
|
@@ -687,12 +697,7 @@ const HEADLESS_LOGIN_HELP = [
|
|
|
687
697
|
async function handleAccounts(action, args, out) {
|
|
688
698
|
const config = await loadConfig();
|
|
689
699
|
if (action === "remove") {
|
|
690
|
-
const removed = findSavedLogin(config.profiles, args.join(" "));
|
|
691
|
-
config.profiles = config.profiles.filter((p) => p.id !== removed.id);
|
|
692
|
-
if (config.lastProfileId === removed.id)
|
|
693
|
-
config.lastProfileId = config.profiles[0]?.id ?? null;
|
|
694
|
-
await saveConfig(config);
|
|
695
|
-
const session = await endRemovedSession(removed);
|
|
700
|
+
const { removed, ...session } = await removeLogin(findSavedLogin(config.profiles, args.join(" ")).id);
|
|
696
701
|
const result = { status: "removed", wilma: removed.tenantName ?? removed.tenantUrl, username: removed.username, ...session };
|
|
697
702
|
out.data(result, () => session.session === "ended"
|
|
698
703
|
? `Removed ${removed.username} @ ${result.wilma} and ended its Wilma session.`
|
|
@@ -701,39 +706,29 @@ async function handleAccounts(action, args, out) {
|
|
|
701
706
|
: `Removed ${removed.username} @ ${result.wilma}.`);
|
|
702
707
|
return;
|
|
703
708
|
}
|
|
704
|
-
const accounts = config.profiles.map((p, i) =>
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
709
|
+
const accounts = config.profiles.map((p, i) => {
|
|
710
|
+
// As Wilma said when the login's children were last read.
|
|
711
|
+
const role = roleOf(p.students ?? []);
|
|
712
|
+
const hidden = (p.students ?? []).filter((st) => p.hidden?.includes(st.studentNumber)).map((st) => st.name);
|
|
713
|
+
return {
|
|
714
|
+
number: i + 1,
|
|
715
|
+
wilma: p.tenantName ?? p.tenantUrl,
|
|
716
|
+
url: p.tenantUrl,
|
|
717
|
+
username: p.username,
|
|
718
|
+
...(role ? { role } : {}),
|
|
719
|
+
children: (p.students ?? []).filter((st) => !p.hidden?.includes(st.studentNumber)).map((st) => st.name),
|
|
720
|
+
// Unticked on the login page: not read.
|
|
721
|
+
...(hidden.length ? { hidden } : {}),
|
|
722
|
+
};
|
|
723
|
+
});
|
|
711
724
|
out.data({ accounts }, () => accounts.length
|
|
712
725
|
? [
|
|
713
|
-
...accounts.map((a) => `${a.number}. ${a.wilma} — ${a.username}${a.children.length ? ` (${a.children.join(", ")})` : ""}`),
|
|
726
|
+
...accounts.map((a) => `${a.number}. ${a.wilma} — ${a.username}${a.children.length ? ` (${a.children.join(", ")})` : ""}${a.role === "student" ? ", the student's own login" : ""}${a.hidden ? `; not read: ${a.hidden.join(", ")}` : ""}`),
|
|
714
727
|
"",
|
|
715
728
|
"Add another with `wilma login`; remove one with `wilma accounts remove <number>`.",
|
|
716
729
|
].join("\n")
|
|
717
730
|
: "No saved Wilma logins. Run `wilma login`.");
|
|
718
731
|
}
|
|
719
|
-
/**
|
|
720
|
-
* End a removed login's Wilma session and forget it; the other logins keep
|
|
721
|
-
* theirs. The login is removed even when Wilma can't be reached: its saved
|
|
722
|
-
* session is forgotten anyway, and Wilma ends the session when it expires.
|
|
723
|
-
*/
|
|
724
|
-
async function endRemovedSession(removed) {
|
|
725
|
-
const password = revealSecret(removed.passwordObfuscated);
|
|
726
|
-
if (!password)
|
|
727
|
-
return { session: "none" };
|
|
728
|
-
const profile = { baseUrl: removed.tenantUrl, username: removed.username, password };
|
|
729
|
-
try {
|
|
730
|
-
return { session: (await logoutSession(profile)) ? "ended" : "none" };
|
|
731
|
-
}
|
|
732
|
-
catch (err) {
|
|
733
|
-
await forgetSession(profile);
|
|
734
|
-
return { session: "not_ended", message: err instanceof Error ? err.message : String(err) };
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
732
|
/** A saved login by its number in `wilma accounts`, its Wilma's name or address, or its username. */
|
|
738
733
|
function findSavedLogin(profiles, rawTarget) {
|
|
739
734
|
const target = rawTarget.trim().toLowerCase();
|
package/dist/login-server.d.ts
CHANGED
|
@@ -8,11 +8,13 @@ export interface LoginServer {
|
|
|
8
8
|
close(): void;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Serve a one-time login page on 127.0.0.1
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* the
|
|
11
|
+
* Serve a one-time login page on 127.0.0.1, laid out like WilmAI Home's Wilma
|
|
12
|
+
* page: who uses WilmAI, one row per Wilma (a saved one with its children to
|
|
13
|
+
* tick, Log in again and Remove; a new one to fill in), Add another school's
|
|
14
|
+
* Wilma, and one button that logs in every new row. Each verified login is
|
|
15
|
+
* saved to the local config file — the password never passes through an
|
|
16
|
+
* agent's chat. One login per Wilma, as in Home. The page closes when the
|
|
17
|
+
* parent presses Done or after 15 idle minutes.
|
|
16
18
|
*/
|
|
17
19
|
/**
|
|
18
20
|
* The assistant app that asked for the login (by the name it gives when it
|
|
@@ -37,12 +39,30 @@ export type BrowserOpenResult = "opened" | "skipped" | "headless";
|
|
|
37
39
|
export declare function openBrowser(url: string, opts?: {
|
|
38
40
|
dryRun?: boolean;
|
|
39
41
|
}): BrowserOpenResult;
|
|
42
|
+
/** A saved login as the page shows it (see accountsSummary). */
|
|
43
|
+
export interface SavedLogin {
|
|
44
|
+
id: string;
|
|
45
|
+
wilma: string;
|
|
46
|
+
url: string;
|
|
47
|
+
username: string;
|
|
48
|
+
role?: "guardian" | "student";
|
|
49
|
+
children: {
|
|
50
|
+
number: string;
|
|
51
|
+
name: string;
|
|
52
|
+
hidden?: boolean;
|
|
53
|
+
}[];
|
|
54
|
+
}
|
|
40
55
|
export interface LoginPageOptions {
|
|
41
56
|
nonce: string;
|
|
42
57
|
apiBase: string;
|
|
43
58
|
/** Shown in the privacy note under the form. */
|
|
44
59
|
configPath?: string;
|
|
45
60
|
openedBy?: OpenedBy;
|
|
61
|
+
/** The logins already saved. */
|
|
62
|
+
accounts?: SavedLogin[];
|
|
46
63
|
}
|
|
47
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* The login page, laid out like WilmAI Home's Wilma page. The API answers
|
|
66
|
+
* {status: "ok", accounts} after a login, a removal or a tick.
|
|
67
|
+
*/
|
|
48
68
|
export declare function renderLoginPage(opts: LoginPageOptions): string;
|